Compare commits

..

10 Commits

Author SHA1 Message Date
pash
b23ff97ddc Merge remote-tracking branch 'origin/main' into codex/session-transcript-oom-guard
# Conflicts:
#	src/docker-build-cache.test.ts
#	src/scripts/test-projects.test.ts
2026-04-26 17:02:51 -07:00
pash
e705246619 Fix core support boundary test expectations 2026-04-26 17:00:59 -07:00
pash
f936f16cc5 Merge remote-tracking branch 'origin/main' into codex/session-transcript-oom-guard 2026-04-26 16:59:22 -07:00
pash
fd48faa4ed Fix qa-lab merge CI and compaction review notes 2026-04-26 16:53:15 -07:00
pash
29f1cae867 Fix qa-lab private qa-channel import 2026-04-26 16:43:55 -07:00
pash
f58dd36a1d Harden session truncation concurrency guards 2026-04-26 16:34:05 -07:00
pash
33e3dccbea Fix update CLI service state test mocks 2026-04-26 16:29:55 -07:00
pash
6fc954539f Harden session truncation rewrite 2026-04-26 16:20:29 -07:00
pash
fc13a0135e Fix stale e2e Docker cache test 2026-04-26 16:14:58 -07:00
pash
0ced62f512 Fix transcript truncation OOM guard 2026-04-26 16:10:10 -07:00
7260 changed files with 110700 additions and 443462 deletions

View File

@@ -10,28 +10,12 @@ description: Run Blacksmith Testbox for CI-parity checks, secrets, hosted servic
Use Testbox when you need remote CI parity, injected secrets, hosted services,
or an OS/runtime image that your local machine cannot provide cheaply.
For OpenClaw, Crabbox is a supported alternative when Blacksmith is unavailable
or owned cloud capacity is preferable.
Do not default to Testbox for every local test/build loop. If the repo has
documented local commands for normal iteration, use those first so you keep
warm caches, local build state, and fast feedback.
Testbox is the expensive path. Reach for it deliberately.
OpenClaw maintainers can opt into Testbox-first validation by setting
`OPENCLAW_TESTBOX=1` in their environment or standing agent rules. This mode is
maintainers-only and requires Blacksmith access.
When `OPENCLAW_TESTBOX=1` is set in OpenClaw:
- Pre-warm a Testbox early for longer, wider, or uncertain work.
- Prefer Testbox for `pnpm` gates, e2e, package-like proof, and broad suites.
- Reuse the same Testbox ID for every run command in the same task/session.
- Use local commands only when the task explicitly sets
`OPENCLAW_LOCAL_CHECK_MODE=throttled|full`, or when the user asks for local
proof.
## Install the CLI
If `blacksmith` is not installed, install it:
@@ -97,8 +81,7 @@ Prefer Testbox when:
- you are reproducing CI-only failures
- you need the exact workflow image/job environment from GitHub Actions
For OpenClaw specifically, normal local iteration stays local unless maintainer
Testbox mode is enabled with `OPENCLAW_TESTBOX=1`:
For OpenClaw specifically, normal local iteration should stay local:
- `pnpm check:changed`
- `pnpm test:changed`
@@ -106,54 +89,27 @@ Testbox mode is enabled with `OPENCLAW_TESTBOX=1`:
- `pnpm test:serial`
- `pnpm build`
If `OPENCLAW_TESTBOX=1` is enabled, run those same repo commands inside the
warm Testbox. If the user wants laptop-friendly local proof for one command, use
the explicit escape hatch `OPENCLAW_LOCAL_CHECK_MODE=throttled`.
For installable-package product proof, prefer the GitHub `Package Acceptance`
workflow over an ad hoc Testbox command. It resolves one package candidate
(`source=npm`, `source=ref`, `source=url`, or `source=artifact`), uploads it as
`package-under-test`, and runs the reusable Docker E2E lanes against that exact
tarball on GitHub/Blacksmith runners. Use `workflow_ref` for the trusted
workflow/harness code and `package_ref` for the source ref to pack when testing
an older trusted branch, tag, or SHA.
Only use Testbox in OpenClaw when the user explicitly wants CI-parity or the
check truly depends on remote secrets/services that the local repo loop cannot
provide.
## Setup: Warmup before coding
If you decided Testbox is warranted, warm one up early. This returns an ID
instantly and boots the CI environment in the background while you work:
If you decided Testbox is actually warranted, warm one up early. This returns
an ID instantly and boots the CI environment in the background while you work:
blacksmith testbox warmup ci-check-testbox.yml
# → tbx_01jkz5b3t9...
Save this ID in the current session. You need it for every `run` command.
Treat `blacksmith testbox list` as diagnostics, not a reusable work queue.
Listed boxes can be visible at the org/repo level while still being unusable or
stale for the current local agent lane.
For OpenClaw maintainer Testbox mode, pre-warm at the start of longer or wider
tasks:
blacksmith testbox warmup ci-check-testbox.yml --ref main --idle-timeout 90
pnpm testbox:claim --id <ID>
Use the build-artifact warmup when e2e/package/build proof benefits from seeded
`dist/`, `dist-runtime/`, and build-all caches:
blacksmith testbox warmup ci-build-artifacts-testbox.yml --ref main --idle-timeout 90
pnpm testbox:claim --id <ID>
Save this ID. You need it for every `run` command.
Warmup dispatches a GitHub Actions workflow that provisions a VM with the
full CI environment: dependencies installed, services started, secrets
injected, and a clean checkout of the repo at the default branch.
In OpenClaw, raw commit SHAs are not reliable dispatch refs for `warmup --ref`;
use a branch or tag. The build-artifact workflow resolves `openclaw@beta` and
`openclaw@latest` to SHA cache keys internally.
Options:
--ref <branch|tag> Git ref to dispatch against (default: repo's default branch)
--ref <branch> Git ref to dispatch against (default: repo's default branch)
--job <name> Specific job within the workflow (if it has multiple)
--idle-timeout <min> Idle timeout in minutes (default: 30)
@@ -186,26 +142,6 @@ The `run` command automatically waits for the testbox to become ready if
it is still booting, so you can call `run` immediately after warmup without
needing to check status first.
In OpenClaw, prefer the guarded runner wrapper so stale/reused ids fail before
the Blacksmith CLI spends time syncing or emits a confusing missing-key error:
pnpm testbox:run --id <ID> -- "OPENCLAW_TESTBOX=1 pnpm check:changed"
The wrapper refuses to run when the local per-Testbox key is missing or when the
id was not claimed by this OpenClaw checkout with `pnpm testbox:claim --id
<ID>`. Treat that as the expected remediation, not as a GitHub account or
normal SSH-key problem. A local key alone is not enough; a ready box may still
carry stale rsync state from another lane.
If the agent crashes, the remote box relies on Blacksmith's idle timeout. The
local OpenClaw claim marker is not deleted automatically, so the wrapper treats
claims older than 12 hours as stale. Override only for intentional long-running
work with `OPENCLAW_TESTBOX_CLAIM_TTL_MINUTES=<minutes>`.
Before spending a broad gate on a manually assembled command, you can also run:
pnpm testbox:sanity -- --id <ID>
## Downloading files from a testbox
Use the `download` command to retrieve files or directories from a running
@@ -290,11 +226,6 @@ services, CI-only runners, or reproducibility against the workflow image.
If the repo says local tests/builds are the normal path, follow the repo.
OpenClaw maintainer exception: if `OPENCLAW_TESTBOX=1` is set by the user or
agent environment, treat Testbox as the normal validation path for this repo.
Use `OPENCLAW_LOCAL_CHECK_MODE=throttled|full` as the explicit local escape
hatch.
## When to use
Use Testbox when:
@@ -311,26 +242,18 @@ checks that need parity or remote state.
## Workflow
1. Decide whether the repo's local loop is the right default. For OpenClaw,
`OPENCLAW_TESTBOX=1` makes Testbox the maintainer default.
2. If Testbox is warranted, warm up early:
`blacksmith testbox warmup ci-check-testbox.yml --ref main --idle-timeout 90` → save the ID,
then `pnpm testbox:claim --id <ID>`
1. Decide whether the repo's local loop is the right default.
2. Only if Testbox is warranted, warm up early:
`blacksmith testbox warmup ci-check-testbox.yml` → save the ID
3. Write code while the testbox boots in the background.
4. Run the remote command when needed:
`pnpm testbox:run --id <ID> -- "OPENCLAW_TESTBOX=1 pnpm check:changed"`
`blacksmith testbox run --id <ID> "npm test"`
5. If tests fail, fix code and re-run against the same warm box.
6. If you changed dependency manifests (package.json, etc.), prepend
the install command: `blacksmith testbox run --id <ID> "npm install && npm test"`
7. If a narrow PR reports a full sync or the box was reused/expired, sanity
check the remote copy before a slow gate:
`pnpm testbox:run --id <ID> -- "pnpm testbox:sanity"`.
If it reports missing root files or mass tracked deletions, stop the box and
warm a fresh one. Use `OPENCLAW_TESTBOX_ALLOW_MASS_DELETIONS=1` only for an
intentional large deletion PR.
8. If you need artifacts (coverage reports, build outputs, etc.), download them:
7. If you need artifacts (coverage reports, build outputs, etc.), download them:
`blacksmith testbox download --id <ID> coverage/ ./coverage/`
9. Once green, commit and push.
8. Once green, commit and push.
## OpenClaw full test suite
@@ -345,15 +268,9 @@ Observed full-suite time on Blacksmith Testbox is about 3-4 minutes:
- 173-180s on a warmed box
- 219s on a fresh 32-vCPU box
When validating before commit/push in maintainer Testbox mode, run
`pnpm check:changed` inside the warmed box first when appropriate, then the full
suite with the profile above if broad confidence is needed.
Run `pnpm testbox:sanity` inside the warmed box before the broad command when
the sync looks suspicious. It checks that root files such as `pnpm-lock.yaml`
still exist and fails on 200 or more tracked deletions. That catches stale or
corrupted rsync state before dependency install or Vitest failures hide the real
problem.
When validating before commit/push, run `pnpm check:changed` first when
appropriate, then the full suite with the profile above if broad confidence is
needed.
## Examples
@@ -407,14 +324,12 @@ timeout is reached). Default timeout is 5m; use `--wait-timeout` for longer
blacksmith testbox stop --id <ID>
Testboxes automatically shut down after being idle (default: 30 minutes).
If you need a longer session, increase the timeout at warmup time. For OpenClaw
maintainer work, use 90 minutes for long-running sessions:
If you need a longer session, increase the timeout at warmup time:
blacksmith testbox warmup ci-check-testbox.yml --idle-timeout 90
blacksmith testbox warmup ci-build-artifacts-testbox.yml --idle-timeout 90
blacksmith testbox warmup ci-check-testbox.yml --idle-timeout 60
## With options
blacksmith testbox warmup ci-check-testbox.yml --ref main
blacksmith testbox warmup ci-check-testbox.yml --idle-timeout 90
blacksmith testbox warmup ci-check-testbox.yml --idle-timeout 60
blacksmith testbox run --id <ID> "go test ./..."

View File

@@ -1,339 +0,0 @@
---
name: clawsweeper
description: "Use for all ClawSweeper work: OpenClaw issue/PR sweep reports, commit-review reports, repair jobs, cloud fix PRs, @clawsweeper maintainer mention commands, trusted ClawSweeper-reviewed autofix/automerge, GitHub Actions monitoring, permissions, gates, and manual backfills."
---
# ClawSweeper
ClawSweeper lives at `~/Projects/clawsweeper`. It is the one OpenClaw
maintenance bot for sweeping, commit review, repair jobs, and guarded fix PRs.
Use this skill whenever Peter asks about reports, findings, dispatch health,
repair/cloud PR creation, comment commands, automerge, permissions, or gates.
## Start
```bash
cd ~/Projects/clawsweeper
git status --short --branch
git pull --ff-only
pnpm run build:all
```
Do not overwrite unrelated edits. If the tree is dirty, inspect first and keep
read-only report work read-only unless Peter asked to commit.
## One Bot, One App
Use the ClawSweeper repo and the `clawsweeper` GitHub App. Use only
`CLAWSWEEPER_*` configuration for this automation. Do not use legacy apps,
variables, labels, or skills.
Required app setup:
- `CLAWSWEEPER_APP_CLIENT_ID`: public app client ID for `clawsweeper`.
- `CLAWSWEEPER_APP_PRIVATE_KEY`: private key used only inside
`actions/create-github-app-token` steps.
- Target app permissions: read target scan context; write issues and pull
requests; contents write for report commits, repair branches, and workflow
inputs; Actions write on `openclaw/clawsweeper` for comment-router
re-review dispatch, workflow dispatch, run cancellation, and self-heal;
optional Checks write for commit Check Runs.
Token boundary:
- Codex workers do not get mutation credentials.
- Review workers run with stripped secret/token env.
- Deterministic scripts own comments, labels, branch pushes, PR creation,
closes, and merges through short-lived GitHub App tokens.
- Merge and write gates default closed.
## Commit Reports
Canonical commit reports:
```text
records/<repo-slug>/commits/<40-char-sha>.md
```
Use the lister:
```bash
pnpm commit-reports -- --since 6h
pnpm commit-reports -- --since "24 hours ago" --findings
pnpm commit-reports -- --since 7d --non-clean
pnpm commit-reports -- --repo openclaw/openclaw --author steipete --since 7d
pnpm commit-reports -- --since 24h --json
```
Results: `nothing_found`, `findings`, `inconclusive`, `failed`,
`skipped_non_code`. One report per SHA; reruns overwrite the SHA-named report.
Manual rerun/backfill:
```bash
gh workflow run commit-review.yml --repo openclaw/clawsweeper \
-f target_repo=openclaw/openclaw \
-f commit_sha=<end-sha> \
-f before_sha=<start-or-parent-sha> \
-f create_checks=false \
-f enabled=true
```
Use `create_checks=true` only when Peter explicitly wants target commit Check
Runs. Add `-f additional_prompt="..."` for focused one-off review instructions.
## Sweep Reports
Issue/PR reports live at:
```text
records/<repo-slug>/items/<number>.md
records/<repo-slug>/closed/<number>.md
```
Lead with counts, concrete findings, and report links. Do not post unsolicited
GitHub comments from report-reading work. Public surfaces are markdown reports,
durable ClawSweeper review comments, and optional checks.
PR reports include Codex `/review`-style `reviewFindings` with priority,
confidence, repository-relative file, and line range. Public PR comments show a
short `Review findings:` list when findings exist; full review comments,
evidence links, likely owners, and runtime details stay inside the collapsed
`Review details` block.
Useful commands:
```bash
pnpm run status
pnpm run audit
pnpm run reconcile
pnpm run apply-decisions -- --dry-run
```
## Create One Repair Job
Create a job from issue/PR refs and a maintainer prompt:
```bash
pnpm run repair:create-job -- \
--repo openclaw/openclaw \
--refs 123,456 \
--prompt-file /tmp/clawsweeper-prompt.md
```
Create from an existing ClawSweeper report:
```bash
pnpm run repair:create-job -- \
--from-report ../clawsweeper/records/openclaw-openclaw/items/123.md
```
The job creator checks for an existing open PR, body match, or remote
`clawsweeper/<cluster-id>` branch before writing another job. Use `--dry-run`
to inspect. Use `--force` only after deciding the duplicate guard is stale.
Validate, commit, then dispatch:
```bash
pnpm run repair:validate-job -- jobs/openclaw/inbox/clawsweeper-openclaw-openclaw-123.md
pnpm run repair:dispatch -- jobs/openclaw/inbox/clawsweeper-openclaw-openclaw-123.md \
--mode autonomous \
--runner blacksmith-4vcpu-ubuntu-2404 \
--execution-runner blacksmith-16vcpu-ubuntu-2404 \
--model gpt-5.5
```
Do not dispatch a just-created job before the job file is committed and pushed;
the workflow reads the job path from GitHub.
## Replacement PRs
For a useful but uneditable/stale/unsafe source PR, make the maintainer prompt
explicit:
```md
Treat #123 as useful source work. If the source branch cannot be safely updated
because it is uneditable, stale, draft-only, unmergeable, or unsafe, create a
narrow ClawSweeper replacement PR instead of waiting. Preserve the source PR
author as co-author, credit the source PR in the replacement PR body, and close
only that source PR after the replacement PR is opened.
```
The worker should emit `repair_strategy=replace_uneditable_branch` and list the
source PR URL in `source_prs`. The deterministic executor opens or updates
`clawsweeper/<cluster-id>`, adds non-bot source authors as `Co-authored-by`
trailers, and closes superseded source PRs only after replacement exists.
## Gates
Open execution windows intentionally and close them after the run:
```bash
gh variable set CLAWSWEEPER_ALLOW_EXECUTE --repo openclaw/clawsweeper --body 1
gh variable set CLAWSWEEPER_ALLOW_FIX_PR --repo openclaw/clawsweeper --body 1
gh variable set CLAWSWEEPER_ALLOW_MERGE --repo openclaw/clawsweeper --body 1
gh variable set CLAWSWEEPER_ALLOW_AUTOMERGE --repo openclaw/clawsweeper --body 1
```
Reset gates only when Peter asks; the active maintainer window may intentionally
leave them at `1`.
Important gates:
- `CLAWSWEEPER_ALLOW_EXECUTE`: allows deterministic write lanes.
- `CLAWSWEEPER_ALLOW_FIX_PR`: allows branch repair/replacement PRs.
- `CLAWSWEEPER_ALLOW_MERGE`: allows merge-capable applicators.
- `CLAWSWEEPER_ALLOW_AUTOMERGE`: allows comment-router automerge.
- `CLAWSWEEPER_COMMENT_ROUTER_EXECUTE`: lets scheduled comment routing
post replies and dispatch repair.
## Maintainer Mentions
Prefer `@clawsweeper` comments for all maintainer-facing control. Slash
commands still parse as compatibility aliases, but examples and live guidance
should use mentions.
```text
@clawsweeper status
@clawsweeper re-review
@clawsweeper review
@clawsweeper fix ci
@clawsweeper address review
@clawsweeper rebase
@clawsweeper autofix
@clawsweeper automerge
@clawsweeper approve
@clawsweeper explain
@clawsweeper stop
@clawsweeper <question or safe action request>
@clawsweeper[bot] re-review
@openclaw-clawsweeper fix ci
@openclaw-clawsweeper[bot] fix ci
```
Accepted aliases: `review`, `re-review`, `rereview`, `review again`,
`rerun review`, and `run review`. `review` and `re-review` dispatch a fresh
ClawSweeper issue/PR review without starting repair. `fix ci`,
`address review`, and `rebase` dispatch the
repair worker only for ClawSweeper PRs or PRs opted into
`clawsweeper:autofix` or `clawsweeper:automerge`. `autofix` runs the bounded
review/fix loop without merging. `automerge` runs the bounded review/fix/merge
loop, but draft PRs stay fix-only until GitHub marks them ready for review.
Freeform maintainer mentions such as `@clawsweeper why did automerge stop?`
or `@clawsweeper: can you explain this failure?` dispatch a read-only assist
review with the mention text as one-off instructions. The answer lands in the
next public ClawSweeper review comment. Action-looking prose does not directly
mutate GitHub; it must map to existing structured recommendations and pass the
normal deterministic gates.
Default accepted maintainers: `OWNER`, `MEMBER`, `COLLABORATOR`; fallback
repository permission accepts `admin`, `maintain`, or `write`. Contributor
comments are ignored without a reply.
Run router manually:
```bash
pnpm run repair:comment-router -- --repo openclaw/openclaw --lookback-minutes 180
pnpm run repair:comment-router -- --repo openclaw/openclaw --execute --wait-for-capacity
```
Scheduled routing stays dry unless
`CLAWSWEEPER_COMMENT_ROUTER_EXECUTE=1`.
## Trusted Autofix And Automerge
`@clawsweeper autofix` opts an existing PR into the bounded review/fix loop.
`@clawsweeper automerge` opts an existing PR into the bounded review/fix/merge
loop. The router:
- verifies maintainer authorization;
- labels the PR `clawsweeper:autofix` or `clawsweeper:automerge`;
- dispatches ClawSweeper review for the current head SHA;
- creates or reuses a durable adopted job;
- repairs at most the configured caps;
- never merges autofix PRs or draft PRs;
- merges automerge PRs only when ClawSweeper passed the exact current head,
checks are green, GitHub says mergeable, no human-review label is present,
the PR is not draft, required user-facing OpenClaw changelog entries are
present, and both merge gates are open.
If ClawSweeper passes while merge gates are closed, it labels
`clawsweeper:merge-ready` and comments instead of merging. `@clawsweeper stop`
adds `clawsweeper:human-review`.
When Peter asks Codex to create a PR and enable ClawSweeper automerge, do not
leave his local OpenClaw checkout on the PR branch. After the PR is created,
pushed, and the `@clawsweeper automerge` request is posted or otherwise
confirmed, return the local checkout to `main` and fast-forward it when the
working tree is clean:
```bash
git switch main
git pull --ff-only
```
If unrelated local edits or an in-progress rebase prevent switching, report the
blocker instead of stashing, deleting, or overwriting work.
Repair caps:
```bash
CLAWSWEEPER_MAX_REPAIRS_PER_PR=10
CLAWSWEEPER_MAX_REPAIRS_PER_HEAD=1
```
## Security Boundary
Do not stage unapproved security-sensitive work for ClawSweeper Repair. Route
vulnerability reports, CVE/GHSA/advisory work, leaked secrets/tokens/keys,
plaintext secret storage, SSRF, XSS, CSRF, RCE, auth bypass, privilege
escalation, and sensitive data exposure to central OpenClaw security handling.
For PRs explicitly opted into `clawsweeper:autofix` or
`clawsweeper:automerge`, security-sensitive review findings may dispatch
bounded repair, but merge remains blocked until a later exact-head review is
clean and the normal merge gates pass. Trust deterministic ClawSweeper security
markers, labels, and job frontmatter; do not infer security handling from vague
prose.
## Monitoring
Receiver workflows:
```bash
gh run list --repo openclaw/clawsweeper --workflow "ClawSweeper Commit Review" \
--limit 12 --json databaseId,displayTitle,event,status,conclusion,createdAt,updatedAt,url
gh run list --repo openclaw/clawsweeper --workflow "repair cluster worker" \
--limit 12 --json databaseId,displayTitle,event,status,conclusion,createdAt,updatedAt,url
gh run list --repo openclaw/clawsweeper --workflow "repair comment router" \
--limit 12 --json databaseId,displayTitle,event,status,conclusion,createdAt,updatedAt,url
```
Target dispatcher:
```bash
gh run list --repo openclaw/openclaw --workflow "ClawSweeper Dispatch" \
--event push --limit 8 --json databaseId,displayTitle,event,status,conclusion,headSha,url
```
Target commit check:
```bash
gh api "repos/openclaw/openclaw/commits/<sha>/check-runs?per_page=100" \
--jq '.check_runs[] | select(.name=="ClawSweeper Commit Review") | [.status,.conclusion,.details_url] | @tsv'
```
## Reading Output
For findings or failures, summarize:
- target repo, item/PR/commit, run, report path
- result, confidence, severity, and exact blocker
- affected files or cluster refs
- validation commands and whether they passed
- whether mutation gates were open or closed
- next deterministic action
Keep the broom small: one cluster, one branch, one PR, narrow proof, clear
owner-visible evidence.

View File

@@ -1,4 +0,0 @@
interface:
display_name: "ClawSweeper"
short_description: "Inspect ClawSweeper commit review reports and Actions runs."
default_prompt: "Review recent ClawSweeper commit reports and summarize findings."

View File

@@ -1,81 +0,0 @@
---
name: crabbox
description: Use Crabbox for OpenClaw remote Linux validation, warmed reusable boxes, GitHub Actions hydration, sync timing, logs, results, caches, and lease cleanup.
---
# Crabbox
Use Crabbox when OpenClaw needs remote Linux proof on owned capacity, a large
runner class, reusable warm state, or a Blacksmith alternative.
## Before Running
- Run from the repo root. Crabbox sync mirrors the current checkout.
- Prefer local targeted tests for tight edit loops.
- Prefer Blacksmith Testbox when the task explicitly asks for Blacksmith or a
Blacksmith-specific CI comparison.
- Use Crabbox for broad OpenClaw gates when owned AWS/Hetzner capacity is the
right remote lane.
- Check `.crabbox.yaml` for repo defaults before adding flags.
- Install with `brew install openclaw/tap/crabbox`; auth is required before use:
`printf '%s' "$CRABBOX_COORDINATOR_TOKEN" | crabbox login --url https://crabbox-coordinator.steipete.workers.dev --provider aws --token-stdin`.
- On macOS the user config is `~/Library/Application Support/crabbox/config.yaml`;
it must include `broker.url`, `broker.token`, and usually `provider: aws`.
## OpenClaw Flow
Warm a reusable box:
```sh
pnpm crabbox:warmup -- --idle-timeout 90m
```
Hydrate it through the repository workflow:
```sh
pnpm crabbox:hydrate -- --id <cbx_id-or-slug>
```
Run broad proof:
```sh
pnpm crabbox:run -- --id <cbx_id-or-slug> --shell "OPENCLAW_TESTBOX=1 pnpm check:changed"
pnpm crabbox:run -- --id <cbx_id-or-slug> --shell "corepack enable && pnpm install --frozen-lockfile && pnpm test"
```
Stop boxes you created before handoff:
```sh
pnpm crabbox:stop -- <cbx_id-or-slug>
```
## Useful Commands
```sh
crabbox status --id <id-or-slug> --wait
crabbox inspect --id <id-or-slug> --json
crabbox sync-plan
crabbox history --lease <id-or-slug>
crabbox logs <run_id>
crabbox results <run_id>
crabbox cache stats --id <id-or-slug>
crabbox ssh --id <id-or-slug>
```
Use `--debug` on `run` when measuring sync timing.
## Hydration Boundary
`.github/workflows/crabbox-hydrate.yml` is repo-specific on purpose. It owns
OpenClaw checkout, setup-node, pnpm setup, provider env hydration, ready marker,
and keepalive. Crabbox owns runner registration, workflow dispatch, SSH sync,
command execution, logs/results, local lease claims, and idle cleanup.
Do not add OpenClaw-specific setup to Crabbox. Put repo setup in the hydration
workflow and generic lease/sync behavior in Crabbox.
## Cleanup
Crabbox has coordinator-owned idle expiry and local lease claims, so OpenClaw
does not need a custom ledger. Default idle timeout is 30 minutes unless config
or flags set a different value. Still stop boxes you created when done.

View File

@@ -1,37 +0,0 @@
---
name: discord-clawd
description: Use to talk to the Discord-backed OpenClaw agent/session; not for archive search.
---
# Discord Clawd
Use this when the task is to talk with the Discord-backed agent/session, ask it a question, or post through that route.
For Discord archive/history/search, use `$discrawl` instead.
## Transport
Use the OpenClaw relay helper:
```bash
cd ~/Projects/agent-scripts
python3 skills/openclaw-relay/scripts/openclaw_relay.py targets
python3 skills/openclaw-relay/scripts/openclaw_relay.py resolve --target maintainers
```
If the target alias exists, prefer a private ask first:
```bash
python3 skills/openclaw-relay/scripts/openclaw_relay.py ask \
--target maintainers \
--message "Reply with exactly OK."
```
Use `publish` when the session should decide whether to post. Use `force-send` only when the user explicitly wants a message posted.
## Guardrails
- Resolve the target before sending real content.
- Report the target and delivery mode used.
- Do not use this for local Discord archive queries.
- Do not expose gateway tokens or session secrets.

View File

@@ -1,4 +0,0 @@
interface:
display_name: "Discord Clawd"
short_description: "Talk to the Discord-backed OpenClaw agent"
default_prompt: "Use $discord-clawd to route a private ask or explicit post through the Discord-backed OpenClaw agent/session."

View File

@@ -1,68 +0,0 @@
---
name: gitcrawl
description: Use gitcrawl for OpenClaw issue and PR archive search, duplicate discovery, related-thread clustering, and local GitHub mirror freshness checks.
metadata:
openclaw:
requires:
bins:
- gitcrawl
---
# Gitcrawl
Use this skill before live GitHub search when triaging OpenClaw issues or PRs.
`gitcrawl` is the local candidate-discovery layer. It is fast, includes open and closed threads, and can surface duplicate attempts, related issues, and already-landed fixes. It is not the final source of truth for comments, labels, merges, closes, or current CI.
## Default Flow
1. Check local state:
```bash
gitcrawl doctor --json
```
2. Read the target from the local archive:
```bash
gitcrawl threads openclaw/openclaw --numbers <issue-or-pr-number> --include-closed --json
```
3. Find related candidates:
```bash
gitcrawl neighbors openclaw/openclaw --number <issue-or-pr-number> --limit 12 --json
gitcrawl search openclaw/openclaw --query "<scope or title keywords>" --mode hybrid --limit 20 --json
```
4. Inspect relevant clusters:
```bash
gitcrawl cluster-detail openclaw/openclaw --id <cluster-id> --member-limit 20 --body-chars 280 --json
```
5. Verify anything actionable with live GitHub and the checkout:
```bash
gh pr view <number> --json number,title,state,mergedAt,body,files,comments,reviews,statusCheckRollup
gh issue view <number> --json number,title,state,body,comments,closedAt
```
## Freshness Rules
- Treat `gitcrawl` as stale if `doctor` shows no target thread, an old `last_sync_at`, missing embeddings for neighbor/search commands, or a clearly wrong open/closed state.
- If stale data blocks the decision, refresh the portable store first:
```bash
gitcrawl init --portable-store git@github.com:openclaw/gitcrawl-store.git --json
```
- Run expensive update commands such as `gitcrawl sync --include-comments` only when the user asked to update the local store or stale data is blocking the decision.
- The sync default is all GitHub thread states; pass `--state open`, `--state closed`, or `--state all` only when a task requires a narrower or explicit scope.
## Boundaries
- Use `gitcrawl` for candidates, clusters, and historical context.
- Use `gh`, `gh api`, and the current checkout for live state before commenting, labeling, closing, reopening, merging, or filing a PR review.
- Do not close or label based only on `gitcrawl` similarity. Require matching problem intent plus live verification.
- If `gitcrawl` is unavailable, say so and fall back to targeted `gh search` rather than blocking normal maintainer work.

View File

@@ -1,4 +0,0 @@
interface:
display_name: "Gitcrawl"
short_description: "Search local OpenClaw issue and PR history before live GitHub triage"
default_prompt: "Use $gitcrawl to inspect OpenClaw issue and PR history, find related threads and duplicate candidates, then verify actionable decisions with live GitHub."

View File

@@ -7,21 +7,20 @@ description: Review, triage, close, label, comment on, or land OpenClaw PRs/issu
Use this skill for maintainer-facing GitHub workflow, not for ordinary code changes.
## Start issue and PR triage with gitcrawl
## Start issue and PR triage with ghcrawl
- Use `$gitcrawl` first anytime you inspect OpenClaw issues or PRs.
- Check local `gitcrawl` data first for related threads, duplicate attempts, and already-landed fixes.
- Use `gitcrawl` for candidate discovery and clustering; use `gh`, `gh api`, and the current checkout to verify live state before commenting, labeling, closing, or landing.
- If `gitcrawl` is missing, stale, lacks the target thread, or has no embeddings for neighbor/search commands, fall back to the GitHub search workflow below.
- Do not run expensive/update commands such as `gitcrawl sync --include-comments`, future enrichment commands, or broad reclustering unless the user asked to update the local store or stale data is blocking the decision.
- Anytime you inspect OpenClaw issues or PRs, check local `ghcrawl` data first for related threads, duplicate attempts, and already-landed fixes.
- Use `ghcrawl` for candidate discovery and clustering; use `gh`, `gh api`, and the current checkout to verify live state before commenting, labeling, closing, or landing.
- If `ghcrawl` is missing, stale, lacks the target thread, or has no embeddings for neighbor/search commands, fall back to the GitHub search workflow below.
- Do not run expensive/update commands such as `ghcrawl refresh`, `ghcrawl embed`, or `ghcrawl cluster` unless the user asked to update the local store or the stale data is blocking the decision.
Common read-only path:
```bash
gitcrawl threads openclaw/openclaw --numbers <issue-or-pr-number> --include-closed --json
gitcrawl neighbors openclaw/openclaw --number <issue-or-pr-number> --limit 12 --json
gitcrawl search openclaw/openclaw --query "<scope or title keywords>" --mode hybrid --json
gitcrawl cluster-detail openclaw/openclaw --id <cluster-id> --member-limit 20 --body-chars 280 --json
ghcrawl threads openclaw/openclaw --numbers <issue-or-pr-number> --include-closed --json
ghcrawl neighbors openclaw/openclaw --number <issue-or-pr-number> --limit 12 --json
ghcrawl search openclaw/openclaw --query "<scope or title keywords>" --mode hybrid --json
ghcrawl cluster-detail openclaw/openclaw --id <cluster-id> --member-limit 20 --body-chars 280 --json
```
## Apply close and triage labels correctly
@@ -41,28 +40,6 @@ gitcrawl cluster-detail openclaw/openclaw --id <cluster-id> --member-limit 20 --
- `invalid`
- `dirty` for PRs only
## Select small high-confidence triage candidates
When asked for `X` issues or PRs to triage, `X` means qualified candidates, not sampled threads.
Only list candidates that pass all gates:
- small owner/surface, with a likely narrow fix and focused regression test
- symptom is reproducible or provable with logs, failing test, live command, dependency contract, or current-main behavior
- root cause is traceable to code with file/line and the proposed fix touches that path
- no strong smell that a broader refactor, ownership rethink, migration, or product decision is the better fix
- dependency-backed behavior checked against upstream docs/source/types; live or web proof used when local proof is insufficient
Loop:
1. Use `gitcrawl` / `gh` to gather candidate clusters.
2. Read issue/PR body, comments, current code, adjacent tests, and dependency contracts.
3. Try focused repro or proof.
4. Reject unclear, stale, speculative, broad-refactor, or owner-ambiguous items.
5. Continue until `X` qualified candidates or the bounded search is exhausted.
Output only qualifying candidates, with: ref, surface, proof, cause, fix sketch, why small, expected test/gate. If none qualify, say so; do not pad.
## Enforce the bug-fix evidence bar
- Never merge a bug-fix PR based only on issue text, PR text, or AI rationale.
@@ -98,7 +75,7 @@ Output only qualifying candidates, with: ref, surface, proof, cause, fix sketch,
## Search broadly before deciding
- Prefer `gitcrawl` first. Then use targeted GitHub keyword search to verify gaps, live status, comments, and candidates not present in the local store.
- Prefer `ghcrawl` first. Then use targeted GitHub keyword search to verify gaps, live status, comments, and candidates not present in the local store.
- Use `--repo openclaw/openclaw` with `--match title,body` first when using `gh search`.
- Add `--match comments` when triaging follow-up discussion or closed-as-duplicate chains.
- Do not stop at the first 500 results when the task requires a full search.

View File

@@ -1,234 +0,0 @@
---
name: openclaw-pre-release-plugin-testing
description: Plan and run pre-release OpenClaw plugin validation across bundled plugins, package artifacts, lifecycle commands, doctor/fix, config round-trip, gateway startup, SDK compatibility, Docker E2E, Package Acceptance, and Testbox proof.
---
# OpenClaw Pre-Release Plugin Testing
Use this skill when the user asks for plugin release confidence, plugin lifecycle
sweeps, package-artifact plugin proof, or "what else should we test before
release?" It complements `openclaw-testing`; use that skill too when choosing
the cheapest safe runner or debugging a failing lane.
## Goal
Prove the plugin system as a product surface, not just as source tests:
- bundled plugin lifecycle: install, inspect, enable, disable, uninstall
- package artifact behavior from a clean `HOME`
- doctor/fix/config validation and idempotence
- config discovery and config round-trip
- status/log visibility and diagnostics
- gateway startup/bootstrap with plugin metadata snapshots
- public SDK compatibility for real external plugins
- live-ish provider/channel probes only when safe credentials exist
## First Checks
From the OpenClaw repo root:
```bash
pnpm docs:list
git status --short --branch
readlink node_modules
pnpm changed:lanes --json
```
In Codex worktrees under `.codex/worktrees`, `node_modules` must be a symlink to
the main OpenClaw checkout. Do not run `pnpm install` there. For broad or
package-heavy proof, use Blacksmith Testbox or GitHub Actions.
## Runner Choice
Prefer this order:
1. **GitHub Package Acceptance** for installable-package product proof.
2. **`ci-build-artifacts-testbox.yml` Testbox** when Docker/package lanes need
seeded `dist`, `dist-runtime`, and package caches.
3. **`ci-check-testbox.yml` Testbox** for source checks, targeted Vitest,
package-boundary checks, or focused Docker lanes.
4. **Local targeted commands only** for small format/static/unit probes.
Avoid long package Docker runs from a stale sparse worktree. If Testbox sync
reports hundreds of changed files or starts deleting package inputs, stop and
warm a fresh box from current `main`, or switch to Package Acceptance.
## Existing Baseline
Run or verify these before inventing new coverage:
```bash
OPENCLAW_TESTBOX=1 pnpm check:changed
pnpm run test:extensions:package-boundary:canary
pnpm run test:extensions:package-boundary:compile
pnpm test:docker:plugins
OPENCLAW_PLUGINS_E2E_CLAWHUB=0 pnpm test:docker:plugins
pnpm test:docker:plugin-update
pnpm test:docker:bundled-channel-deps:fast
```
For full bundled install/uninstall proof, shard the packaged sweep:
```bash
OPENCLAW_BUNDLED_PLUGIN_SWEEP_TOTAL=8 \
OPENCLAW_BUNDLED_PLUGIN_SWEEP_INDEX=<0-7> \
pnpm test:docker:bundled-plugin-install-uninstall
```
Expected current packaged scope: 116 public bundled plugins over shards `0-7`.
Private QA plugins are source-mode only unless a package explicitly includes
them.
## Confidence Matrix
Use this matrix for pre-release signoff. Record pass/fail, run URL/Testbox ID,
package SHA/version, and skipped-live reason.
| Surface | Proof | Preferred runner |
| --- | --- | --- |
| Package artifact | Package Acceptance `suite_profile=package` or custom lanes | GitHub Actions |
| Bundled lifecycle | 8-shard `test:docker:bundled-plugin-install-uninstall` | Testbox or release Docker |
| External plugins | `test:docker:plugins` and `plugins-offline` | Testbox/package acceptance |
| Update no-op | `test:docker:plugin-update` | Testbox/package acceptance |
| Channel runtime deps | `test:docker:bundled-channel-deps:fast` plus key channels | Testbox/package acceptance |
| Doctor/fix | seeded bad configs + `doctor --fix --non-interactive` | new Docker/Testbox harness |
| Config round-trip | `config set/get`, inspect, doctor, reload, diff hash | new Docker/Testbox harness |
| Gateway bootstrap | clean `HOME`, plugin groups enabled/disabled, status JSON | new Docker/Testbox harness |
| SDK compatibility | directory, tgz, and `file:` external plugins using SDK subpaths | `test:docker:plugins` plus new smoke |
| Live-ish | redacted provider/channel probes only for present env | Testbox live lanes |
## Package Acceptance Plan
Use this when validating a release branch, beta, or candidate package:
```bash
gh workflow run package-acceptance.yml \
--repo openclaw/openclaw \
--ref main \
-f workflow_ref=main \
-f source=ref \
-f package_ref=<branch-or-sha> \
-f suite_profile=custom \
-f docker_lanes='plugins-offline plugin-update bundled-channel-deps-compat doctor-switch update-channel-switch config-reload mcp-channels npm-onboard-channel-agent' \
-f telegram_mode=mock-openai
```
Use `source=npm -f package_spec=openclaw@beta` for published beta proof. Keep
`workflow_ref` as trusted current harness code unless the release process says
otherwise.
## New Testbox Harness Plan
If more certainty is needed, add or run a `plugin-lifecycle-matrix` Docker lane
that uses one package tarball and sharded plugin lists. Per plugin:
1. Start with a clean `HOME`.
2. Capture `plugins list --json`.
3. `plugins install <id>`.
4. `plugins inspect <id> --json`.
5. `plugins disable <id>`, then assert disabled visibility.
6. `plugins enable <id>`, except config-required plugins without config.
7. `plugins registry --refresh`.
8. `doctor --non-interactive`.
9. `plugins uninstall <id> --force`.
10. Assert no config entry, allow/deny residue, install record, managed dir, or
bundled `dist/extensions/...` load path remains.
11. Assert diagnostics contain no `level: "error"` and output redacts
secret-looking values.
Keep `memory-lancedb` special: it is config-required. First assert install does
not enable it without embedding config, then run a second configured case.
## Doctor/Fix Matrix
Seed bad states and require `doctor --fix --non-interactive` to repair them,
then run doctor again and require idempotence:
- stale `plugins.allow`
- stale `plugins.entries`
- stale channel config for missing channel plugin
- invalid `plugins.entries.<id>.config`
- packaged bundled path in `plugins.load.paths`
- legacy `plugins.installs`
- disabled channel/plugin config that must not stage runtime deps
- root-owned global package tree that must remain unmodified
## Gateway Bootstrap Matrix
Start packaged OpenClaw in Docker with clean state:
- provider plugins enabled, no credentials: ready with warnings, no crash
- channel plugins configured disabled: no runtime deps staged
- startup-activation plugins enabled: ready and reflected in status
- invalid single plugin config: bad plugin skipped/quarantined, others remain
Assert:
- gateway reaches ready
- `openclaw status --json` includes plugin diagnostics
- `openclaw plugins inspect --all --json` is parseable
- package tree is not mutated
- logs contain no raw tokens
## Config Round-Trip Representatives
Use representative plugin families instead of every plugin for deep config
round-trip:
- providers: `openai`, `anthropic`, `mistral`, `openrouter`
- channels: `telegram`, `discord`, `slack`, `whatsapp`
- memory: `memory-lancedb`
- feature/runtime: `browser`, `acpx`, `tokenjuice`
For each representative:
1. Write config through CLI when possible.
2. Read it back through `config get` or JSON.
3. Run `plugins inspect`.
4. Run `doctor --non-interactive`.
5. Trigger gateway config reload if applicable.
6. Compare config hash before/after no-op commands.
## External SDK Smoke
In a package Docker lane, create tiny external plugins and install them from:
- local directory
- `.tgz`
- `file:` npm spec
Cover CJS and ESM shapes, plus at least one plugin importing focused
`openclaw/plugin-sdk/*` subpaths. Assert `plugins inspect` sees its tool,
gateway method, CLI command, or service.
## Live-Ish Probe Rules
Before live-ish work, source allowed env in Testbox and generate a redacted
availability matrix: present/missing only, never values.
Only run probes for credentials that exist. Prefer auth/catalog/status probes
over sending user-visible messages. If a probe might contact an external user,
channel, or workspace, stop and ask the user.
## Reporting
Report in this shape:
```text
package/ref:
tbx ids / run urls:
matrix:
bundled lifecycle:
package acceptance:
doctor/fix:
gateway bootstrap:
config round-trip:
sdk external:
live-ish:
failures:
skips:
next highest-value gap:
```
Say clearly when a failure is Testbox sync/env damage rather than product
behavior, and prove that with a clean rerun or current-main comparison.

View File

@@ -1,4 +0,0 @@
interface:
display_name: "OpenClaw Plugin Pre-Release Testing"
short_description: "Plan plugin release validation"
default_prompt: "Use $openclaw-pre-release-plugin-testing to plan or run pre-release OpenClaw plugin validation across package, lifecycle, doctor, gateway, SDK, and live-ish proof."

View File

@@ -62,24 +62,6 @@ scenario through qa-channel, decodes the emitted protobuf spans, and verifies
the exported trace names and privacy contract. It does not require Opik,
Langfuse, or external collector credentials.
## Matrix live profiles
`pnpm openclaw qa matrix` defaults to the full `all` profile. Use explicit
profiles for faster CI/release proof:
```bash
OPENCLAW_QA_MATRIX_NO_REPLY_WINDOW_MS=3000 \
pnpm openclaw qa matrix --profile fast --fail-fast
```
- `fast`: release-critical transport contract, excluding generated image and
deep E2EE recovery inventory.
- `transport`, `media`, `e2ee-smoke`, `e2ee-deep`, `e2ee-cli`: sharded full
Matrix coverage.
- `QA-Lab - All Lanes` uses explicit `fast` Matrix on scheduled runs. Manual
dispatch keeps `matrix_profile=all` as the default and always shards that full
Matrix selection.
## QA credentials and 1Password
- Use `op` only inside `tmux` for QA secret lookup in this repo.

View File

@@ -41,11 +41,9 @@ Use this skill for release and publish-time workflow. Keep ordinary development
recommended replacement can shift as plugin ownership, externalization, and
config footprint move, so do not blindly copy stale replacement annotations
into release notes.
- Do not delete or rewrite beta tags after their matching npm package has been
published. If a pushed beta tag fails preflight before npm publish, delete and
recreate the tag and prerelease at the fixed commit so npm prerelease versions
stay contiguous. If a published beta needs a fix, commit the fix on the
release branch and increment to the next `-beta.N`.
- Do not delete or rewrite beta tags after they leave the machine. If a
published or pushed beta needs a fix, commit the fix on the release branch and
increment to the next `-beta.N`.
- For a beta release train, run the fast local preflight first, publish the
beta to npm `beta`, then run the expensive published-package roster focused
on install/update/Docker/Parallels/NPM Telegram. If anything fails, fix it on
@@ -183,9 +181,6 @@ live`; keep it clearly beta and avoid implying stable promotion.
compact launch post, then publish one focused feature explainer per reply.
Follow-up replies should not repeat "new in VERSION" or the version number
when the thread context already makes it obvious.
- Peter's preferred thread workflow: first agree on the generic launch tweet,
then proceed through follow-up tweets one by one. When he says `next`, provide
or copy the next follow-up only; do not dump the full thread again unless asked.
- Every follow-up tweet should include a docs URL for that specific feature.
Prefer a bare URL over `Docs: <url>` unless the label is needed for clarity.
Keep follow-ups concise: around 160-220 raw characters is usually the sweet
@@ -369,10 +364,8 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
- Any fix after preflight means a new commit. Delete and recreate the tag and
matching GitHub release from the fixed commit, then rerun preflight from
scratch before publishing.
Exception: never delete or recreate a beta tag whose matching npm package has
already been published; increment to the next beta number instead. If only the
pushed tag/prerelease exists and npm publish has not happened, recreate that
same beta tag at the fixed commit.
Exception: never delete or recreate a beta tag that has already been pushed or
published; increment to the next beta number instead.
- For stable mac releases, generate the signed `appcast.xml` before uploading
public release assets so the updater feed cannot lag the published binaries.
- Serialize stable appcast-producing runs across tags so two releases do not
@@ -565,9 +558,6 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
commit, and rerun all relevant preflights from scratch before continuing.
Never reuse old preflight results after the commit changes. For pushed or
published beta tags, do not delete/recreate; increment to the next beta tag.
For preflight-only failures where npm did not publish the beta version,
delete/recreate the same beta tag and prerelease at the fixed commit instead
of skipping a prerelease number.
20. Start `.github/workflows/openclaw-npm-release.yml` from the same branch with
the same tag for the real publish, choose `npm_dist_tag` (`beta` default,
`latest` only when you intentionally want direct stable publish), keep it
@@ -580,9 +570,9 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
for critical fixes that landed after the release branch cut; backport only
important low-risk fixes before starting expensive lanes, or increment to
the next beta if the fix must change the already-published package. If any
lane fails after the beta package is published, fix, commit/push/pull,
increment to the next beta tag, and rerun the affected beta evidence. Once
the beta is live, start remote/manual rosters where they
lane fails after the beta tag/package is pushed or published, fix,
commit/push/pull, increment to the next beta tag, and rerun the affected
beta evidence. Once the beta is live, start remote/manual rosters where they
can overlap safely, but keep local Docker and Parallels load controlled.
Ensure the full expensive roster has passed at least once before
stable/latest promotion. The roster includes the manual Actions >

View File

@@ -1,63 +0,0 @@
---
name: openclaw-small-bugfix-sweep
description: Fix only small, high-certainty OpenClaw bugs from a pasted issue/PR list after deep code review.
---
# OpenClaw Small Bugfix Sweep
Batch workflow for pasted OpenClaw issue/PR refs.
Execute, do not summarize.
Triage does not commit, push, create PRs, comment, close, label, land, or merge.
## Companion Skills
Use `$gitcrawl` first, `$openclaw-pr-maintainer` for live GitHub hygiene, `$github-deep-review` posture for source tracing, and `$openclaw-testing` for proof.
## Loop
For each ref:
1. Read live target with `gh`.
2. Check `gitcrawl` for related, duplicate, closed, or already-fixed threads.
3. Read body, comments, linked refs, changed files, current code, adjacent tests, and dependency contracts when relevant.
4. Trace the real runtime path.
5. For issues: fix locally only if this is a bug, current code proves root cause, the implicated path is clear, and a narrow patch is cleaner than refactor.
6. For PRs: decide `ready-to-merge`, `needs-fixup`, or `skip`; do not alter PR branches unless explicitly asked.
7. Add focused regression proof when practical for local issue fixes or PR readiness checks.
8. Run the smallest meaningful gate.
9. Continue until every pasted ref is fixed or classified.
No subagents unless explicitly requested.
## Skip If
- not a bug
- config/docs/workflow/release/support/dependency/product work
- repro or root cause is uncertain
- larger refactor or owner-boundary change is cleaner
- already fixed on current `main`
- dependency behavior is guessed
- no focused proof is feasible
Skip with terse reason. Do not pad with low-confidence fixes.
## Fix Rules
- owner module first; generic seam only when required
- existing patterns/helpers/types
- no drive-by refactors
- tests near failing surface
- docs only for changed public behavior
- no commit/push/create PR/comment/close/label/land/merge unless explicitly asked
## PR Rules
- `ready-to-merge`: code is good, current head checked, required proof is green or clearly pending only external CI; list for maintainer merge or `@clawsweeper automerge`
- `needs-fixup`: small bug is clear, but PR branch needs changes; list exact files/tests and wait for explicit fix/push/automerge instruction
- `skip`: broad, stale, speculative, config/product/security/release, owner-boundary, or refactor-sized
- if source PR is untrusted/uneditable, do not create a replacement PR during sweep
## Output Shape
Ledger: `fixed-local`, `ready-to-merge`, `needs-fixup`, `skipped`, `needs-human`.
Final: issue files left on disk, PRs ready for merge/automerge, tests/gates, skip reasons.

View File

@@ -7,8 +7,6 @@ description: Investigate OpenClaw pnpm test memory growth, Vitest OOMs, RSS spik
Use this skill for test-memory investigations. Do not guess from RSS alone when heap snapshots are available. Treat snapshot-name deltas as triage evidence, not proof, until retainers or dominators support the call.
For **runtime fixes** (e.g., closure leaks in long-running services like the gateway), see [Validating runtime fixes](#validating-runtime-fixes-not-test-memory) below — that uses a dedicated harness, not the test-parallel snapshot machinery.
## Workflow
1. Reproduce the failing shape first.
@@ -65,38 +63,6 @@ For **runtime fixes** (e.g., closure leaks in long-running services like the gat
Read the top positive deltas first. Large positive growth in module-transform artifacts suggests lane isolation; large positive growth in runtime objects suggests a real leak. If the names alone do not settle it, open the same snapshot pair in DevTools and inspect retainers/dominators for the top rows before declaring root cause.
## Validating runtime fixes (not test-memory)
The workflow above is for diagnosing Vitest worker memory growth. For
validating that a runtime/closure fix actually releases captured state, use the
dedicated harness:
- `pnpm leak:embedded-run` — runs `scripts/embedded-run-abort-leak.ts`. Loops N
aborted runs in a function-shaped scope mimicking `runEmbeddedAttempt`,
writes heap snapshots, and reports a PASS/FAIL verdict on retention growth
using `FinalizationRegistry` for tracked-instance counting plus RSS delta.
Modes:
- `closure-extracted` (default) — production fix shape (helper at module scope).
- `closure-inline` — pre-fix shape (closure inside the runner scope). Use as a
sensitivity check: if it passes you've broken the harness, not fixed a bug.
- `synthetic-leak` — deliberately retains via a module-level bucket. Use to
confirm the harness can detect leaks before trusting a PASS on a real fix.
Snapshots land in `.tmp/embedded-run-abort-leak/`. Diff with the same script
as above:
```
node .agents/skills/openclaw-test-heap-leaks/scripts/heapsnapshot-delta.mjs \
.tmp/embedded-run-abort-leak/baseline-*.heapsnapshot \
.tmp/embedded-run-abort-leak/batch-N-*.heapsnapshot --top 30
```
When fixing a different runtime leak, add a new harness alongside this one
rather than retrofitting it. The fixture function should mimic the lexical
scope of the function where the leak lives, not be a generic abort-loop.
## Output Expectations
When using this skill, report:

View File

@@ -1,13 +1,12 @@
---
name: openclaw-test-performance
description: Benchmark, diagnose, and optimize OpenClaw test and plugin-suite runtime, import hotspots, CPU/RSS, heap growth, and slow coverage paths.
description: Benchmark, diagnose, and optimize OpenClaw test runtime, import hotspots, CPU/RSS, and slow coverage paths.
---
# OpenClaw Test Performance
Use evidence first. The goal is real `pnpm test`, plugin-suite, and
plugin-inspector speed/RSS improvement with coverage intact, not runner tuning by
guesswork.
Use evidence first. The goal is real `pnpm test` speed/RSS improvement with
coverage intact, not runner tuning by guesswork.
## Workflow
@@ -22,9 +21,6 @@ guesswork.
2. Establish a baseline before changing code:
- Prefer `pnpm test:perf:groups --full-suite --allow-failures --output <file>`
for full-suite ranking.
- For bundled plugin breadth, run the smallest relevant `pnpm
test:extensions:batch <plugin[,plugin...]>` or plugin-inspector command
before jumping to the full extension sweep.
- For a scoped hotspot use:
`/usr/bin/time -l pnpm test <file-or-files> --maxWorkers=1 --reporter=verbose`
- For import-heavy suspicion add:
@@ -37,8 +33,6 @@ test:extensions:batch <plugin[,plugin...]>` or plugin-inspector command
passed, capture that as harness/noise and verify the suspect file directly.
4. Pick the next attack by return and risk:
- High return: one file/test dominates seconds or RSS and has a clear root.
- High leverage: one plugin or SDK barrel causes every plugin-inspector or
extension-batch run to load broad runtime.
- Lower risk: static descriptors, target parsing, routing, auth bypass,
setup hints, registry fixtures, or test server lifecycle.
- Higher risk: real memory/runtime behavior, live providers, protocol
@@ -50,8 +44,6 @@ test:extensions:batch <plugin[,plugin...]>` or plugin-inspector command
and pure helpers over broad mocks.
- Reuse suite-level servers/clients when a fresh handshake is irrelevant.
- Keep schedulers/background loops off unless the test proves scheduling.
- In plugin paths, move static metadata into manifest/lightweight artifacts
and keep runtime plugin loads behind explicit execution boundaries.
6. Preserve coverage shape:
- Do not delete a slow integration proof unless the exact production
composition is extracted into a named helper and tested.
@@ -65,90 +57,6 @@ test:extensions:batch <plugin[,plugin...]>` or plugin-inspector command
9. Commit with `scripts/committer "<message>" <paths...>` and push when the
user asked for commits/pushes. Stage only files touched for this attack.
## Plugin-Suite Workflow
Use this section when perf work involves bundled plugins, plugin-inspector, SDK
barrels, package-boundary tests, or extension suites.
1. Map the suite shape first:
- source tests: `pnpm test extensions/<id>` or `pnpm test:extensions:batch <id>`
- package boundaries: `pnpm run test:extensions:package-boundary:canary` and
`pnpm run test:extensions:package-boundary:compile`
- all bundled source tests: `pnpm test:extensions`
- plugin import memory: `pnpm test:extensions:memory -- --json .artifacts/test-perf/extensions-memory.json`
- plugin-inspector/report work: keep report primitives in `plugin-inspector`;
keep wrappers thin and collect peak RSS when the command supports it.
2. Start narrow, then widen:
- one plugin changed: run that plugin's tests and plugin-inspector slice.
- SDK/public barrel changed: add representative provider, channel, memory,
and feature plugins.
- loader/runtime mirror changed: add package-boundary checks and build/package
proof as needed.
- unknown shared plugin behavior: run `test:extensions:batch` groups before
`pnpm test:extensions`.
3. Treat plugin-inspector failures as product signals:
- JSON must parse.
- warnings/errors must be classified, not hidden.
- runtime capture should be quiet and config-tolerant.
- command output should include wall time, exit code, and peak RSS when
available.
4. For broad or package-heavy plugin proof, use Blacksmith Testbox by default on
maintainer machines. Warm once and reuse the same box:
- `blacksmith testbox warmup ci-check-testbox.yml --ref main --idle-timeout 90`
- `blacksmith testbox run --id <ID> "OPENCLAW_TESTBOX=1 pnpm test:extensions:batch <ids>"`
- stop the box when done.
5. If plugin performance is package-artifact sensitive, switch to
`openclaw-pre-release-plugin-testing` and Package Acceptance rather than
trusting source-only timing.
## Metric Collection
Collect at least one stable metric before and after. Prefer the same machine and
same command. For Testbox comparisons, use the same `tbx_...` id when possible.
| Metric | Use for | Preferred source |
| --------------- | ---------------------------------- | --------------------------------------------------------------------------- |
| wall time | user-visible suite cost | `/usr/bin/time -l`, test wrapper duration, Testbox run time |
| Vitest duration | test body/import cost | Vitest output per file/shard |
| import duration | broad barrel/runtime loads | `OPENCLAW_VITEST_IMPORT_DURATIONS=1` |
| max RSS | memory pressure and OOM risk | `/usr/bin/time -l`, `pnpm test:extensions:memory`, wrapper memory summaries |
| CPU/user/sys | CPU-bound vs wait-bound split | `/usr/bin/time -l` locally, Testbox job timing when local CPU is noisy |
| heap snapshots | real leak vs retained module graph | `openclaw-test-heap-leaks` workflow |
Local scoped command with CPU/RSS:
```bash
timeout 240 /usr/bin/time -l pnpm test <file> --maxWorkers=1 --reporter=verbose
```
Plugin import memory profile:
```bash
pnpm build
pnpm test:extensions:memory -- --top 20 --json .artifacts/test-perf/extensions-memory.json
```
Targeted plugin import memory:
```bash
pnpm test:extensions:memory -- --extension discord --extension telegram --skip-combined
```
Heap/RSS escalation:
```bash
OPENCLAW_TEST_MEMORY_TRACE=1 \
OPENCLAW_TEST_HEAPSNAPSHOT_INTERVAL_MS=60000 \
OPENCLAW_TEST_HEAPSNAPSHOT_DIR=.tmp/heapsnap \
OPENCLAW_TEST_WORKERS=2 \
OPENCLAW_TEST_MAX_OLD_SPACE_SIZE_MB=6144 \
pnpm test
```
Use `openclaw-test-heap-leaks` when RSS keeps growing across intervals, workers
OOM, or the suspect command has app-object retention. Do not call RSS growth a
leak until snapshots or retainers support it.
## Common Root Causes
- Full bundled channel/plugin runtime loaded for static data.
@@ -156,12 +64,6 @@ leak until snapshots or retainers support it.
parser would suffice.
- Broad `api.ts`, `runtime-api.ts`, `test-api.ts`, or plugin-sdk barrels pulled
into hot tests.
- SDK root aliases or package barrels pulling focused subpaths back into a broad
plugin graph.
- Plugin-inspector loading runtime code just to render metadata, reports, or CI
policy scores.
- Bundled plugin capture reusing real config/home state instead of synthetic,
redacted, isolated state.
- Partial-real mocks using `importActual()` around broad modules.
- `vi.resetModules()` plus fresh imports in per-test loops.
- Test plugin registry seeded in `beforeAll` while runtime state resets in
@@ -170,10 +72,6 @@ leak until snapshots or retainers support it.
- Runtime/default model/auth selection paid by idle snapshots or fixtures.
- Plugin-owned media/action discovery triggered before checking whether args
contain plugin-owned fields.
- Timings missing from `test/fixtures/test-timings.unit.json`, causing hotspot
files to stay in shared workers.
- Parallel Vitest runs sharing `node_modules/.experimental-vitest-cache` without
distinct `OPENCLAW_VITEST_FS_MODULE_CACHE_PATH` values.
## Benchmark Commands
@@ -199,25 +97,6 @@ pnpm test:perf:groups --full-suite --allow-failures \
--output .artifacts/test-perf/<name>.json
```
Extension batch:
```bash
pnpm test:extensions:batch <plugin[,plugin...]> -- --reporter=verbose
```
All extension tests:
```bash
pnpm test:extensions
```
Package-boundary plugin checks:
```bash
pnpm run test:extensions:package-boundary:canary
pnpm run test:extensions:package-boundary:compile
```
Reuse an existing Vitest JSON report:
```bash
@@ -228,26 +107,19 @@ pnpm test:perf:groups --report <vitest-json> \
## Verification
- Always run the targeted test surface that proves the change.
- For source changes, run `pnpm check:changed` before push; in maintainer
Testbox mode run it in the warmed Testbox.
- For test-only changes, run `pnpm test:changed` or the exact edited tests.
- Run `pnpm check` before commit unless the change is docs-only and the hook
handles it.
- Run `pnpm build` when touching lazy-loading, bundled artifacts, package
boundaries, dynamic imports, build output, or public surfaces.
- For plugin SDK/barrel/runtime changes, add `pnpm plugin-sdk:api:check` or
`pnpm plugin-sdk:api:gen` when the API surface may drift.
- For plugin-suite perf fixes, verify at least one representative plugin batch
plus the changed gate; use Package Acceptance if the bug only exists in a
packed artifact.
- If deps are missing/stale, run `pnpm install` and retry the exact failed
command once.
- Use the report format:
```markdown
| Metric | Before | After | Gain |
| -------------- | -----: | -----: | ------------: |
| File wall time | `Xs` | `Ys` | `-Zs` (`P%`) |
| Max RSS | `XMB` | `YMB` | `-ZMB` (`P%`) |
| CPU user/sys | `X/Ys` | `A/Bs` | explain |
| Metric | Before | After | Gain |
| -------------- | -----: | ----: | ------------: |
| File wall time | `Xs` | `Ys` | `-Zs` (`P%`) |
| Max RSS | `XMB` | `YMB` | `-ZMB` (`P%`) |
```
## Handoff
@@ -255,12 +127,8 @@ pnpm test:perf:groups --report <vitest-json> \
Keep the final concise:
- Root cause.
- Suite/plugin scope.
- Files changed.
- Before/after wall, Vitest/import, CPU, and RSS numbers where available.
- Leak classification if memory was involved: real leak, retained module graph,
or inconclusive.
- Before/after numbers.
- Coverage retained.
- Verification commands.
- Testbox ID or workflow URL for remote proof.
- Commit hash and push status.

View File

@@ -1,6 +1,6 @@
interface:
display_name: "OpenClaw Test Performance"
short_description: "Benchmark tests, plugin suites, CPU, RSS, and heap growth"
default_prompt: "Use $openclaw-test-performance to reassess OpenClaw test and plugin-suite performance, collect wall/import/CPU/RSS metrics, investigate memory growth when needed, fix the next real hotspot without losing coverage, update the report, and commit scoped changes."
short_description: "Benchmark and fix slow OpenClaw tests"
default_prompt: "Use $openclaw-test-performance to reassess the OpenClaw test benchmark, identify the next real hotspot, fix it without losing coverage, update the report, and commit scoped changes."
policy:
allow_implicit_invocation: false

View File

@@ -36,14 +36,6 @@ Prove the touched surface first. Do not reflexively run the whole suite.
- Prefer GitHub Actions for release/Docker proof when the workflow already has the prepared image and secrets.
- Use `scripts/committer "<msg>" <paths...>` when committing; stage only your files.
- If deps are missing, run `pnpm install`, retry once, then report the first actionable error.
- For Blacksmith Testbox proof, reuse only an id warmed and claimed in this
operator session. `blacksmith testbox list` is diagnostics only; a listed id
can have a local key and still carry stale rsync state from another lane.
After warmup, run `pnpm testbox:claim --id <id>`, then prefer
`pnpm testbox:run --id <id> -- "<command>"` for OpenClaw gates so stale
org-visible ids fail fast before syncing. Claims older than 12 hours are
stale unless `OPENCLAW_TESTBOX_CLAIM_TTL_MINUTES` is explicitly set for long
work.
## Local Test Shortcuts
@@ -84,9 +76,6 @@ Use targeted file paths whenever possible. Avoid raw `vitest`; use the repo
- Direct test edits run themselves. Source edits prefer explicit mappings,
sibling `*.test.ts`, then import-graph dependents. Shared harness/config/root
edits are skipped by default unless they have precise mapped tests.
- Shared group-room delivery config and source-reply prompt edits are precise
mapped tests: they run the core auto-reply regressions plus Discord and Slack
delivery tests so cross-channel default changes fail before a PR push.
- Public SDK or contract edits do not automatically run every plugin test.
`check:changed` proves extension type contracts; the agent chooses the
smallest plugin/contract Vitest proof that matches the actual risk.
@@ -107,264 +96,6 @@ gh run view <run-id> --job <job-id> --log
- For cancelled same-branch runs, confirm whether a newer run superseded it.
- Fetch full logs only for failed or relevant jobs.
## GitHub Release Workflows
Use the smallest workflow that proves the current risk. The full umbrella is
available, but it is usually the last step after narrower proof, not the first
rerun after a focused patch.
### Full Release Validation
`Full Release Validation` (`.github/workflows/full-release-validation.yml`) is
the manual "everything before release" umbrella. It resolves a target ref, then
dispatches:
- manual `CI` for the full normal CI graph, with Android enabled via
`include_android=true`
- `Plugin Prerelease` for release-only plugin static checks, extension shards,
the release-only `agentic-plugins` shard, and plugin product Docker lanes
- `OpenClaw Release Checks` for install smoke, cross-OS release checks, live and
E2E checks, Docker release-path suites, OpenWebUI, QA Lab, fast Matrix, and
Telegram release lanes
- optional post-publish Telegram E2E when a package spec is supplied
Run it only when validating an actual release candidate, after broad shared CI
or release orchestration changes, or when explicitly asked:
```bash
gh workflow run full-release-validation.yml \
--repo openclaw/openclaw \
--ref main \
-f ref=<branch-or-sha> \
-f provider=openai \
-f mode=both \
-f release_profile=stable
```
Run the workflow itself from the trusted current ref, normally `--ref main`;
child workflows are dispatched from that same ref even when `ref` points at an
older release branch or tag. Full Release Validation has no separate child
workflow ref input; choose the trusted harness by choosing the workflow run ref.
Use `release_profile=minimum|stable|full` to control live/provider breadth:
`minimum` keeps the fastest OpenAI/core release-critical set, `stable` adds the
stable provider/backend set, and `full` adds the broad advisory provider/media
matrix. Do not make `full` faster by silently dropping suites; optimize setup,
artifact reuse, and sharding instead. The parent verifier job appends a child
overview plus slowest-job tables for child runs; rerun only that verifier after
a child rerun turns green.
Standalone manual `CI` dispatches do not run the plugin prerelease suite, the
extension batch sweep, or the release-only `agentic-plugins` Vitest shard. Those
lanes are intentionally reserved for the separate `Plugin Prerelease` child so
PRs, main pushes, and ad hoc broad CI checks do not spend Docker/package time or
all-plugin runtime time on release-only product coverage.
If a full run is already active on a newer `origin/main`, prefer watching that
run over dispatching a duplicate. Do not cancel release, release-check, or child
workflow runs unless Peter explicitly asks for cancellation.
The child-dispatch jobs record the child run ids. The final
`Verify full validation` job re-queries those child runs and is the canonical
parent gate. If a child workflow failed but was later rerun successfully, rerun
only the failed parent verifier job; do not dispatch a new full umbrella unless
the release evidence is stale.
For bounded recovery after a focused fix, pass `-f rerun_group=<group>`.
Supported umbrella groups are `all`, `ci`, `plugin-prerelease`,
`release-checks`, `install-smoke`, `cross-os`, `live-e2e`, `package`, `qa`,
`qa-parity`, `qa-live`, and `npm-telegram`. Use the narrowest group that covers
the failed box. After a targeted release-check fix, do not restart the full
umbrella by habit: dispatch the matching `rerun_group` and rerun only the parent
verifier/evidence step after the child is green unless the release evidence is
stale. For a single failed live/E2E shard, use
`-f rerun_group=live-e2e -f live_suite_filter=<suite_id>` so the Blacksmith
workflow only spends setup and queue time on that suite.
### Release Evidence
After release-candidate validation or before a release decision, record the
important run ids in the private `openclaw/releases-private` evidence ledger.
Use the manual `OpenClaw Release Evidence`
(`openclaw-release-evidence.yml`) workflow there. It writes durable summaries
under `evidence/<release-id>/` and commits:
- `release-evidence.md`
- `release-evidence.json`
- `index.json`
- `runs/<label>.json`
Use one run per line:
```text
full-release-validation openclaw/openclaw <run-id> blocking
package-acceptance openclaw/openclaw <run-id> blocking
release-checks openclaw/openclaw <run-id> blocking
```
Store summaries, run URLs, artifact metadata, timings, pass/fail state, and
short release-manager notes there. Do not store raw logs, provider
prompts/responses, channel transcripts, signing material, or secret-bearing
config in git; raw logs stay in Actions artifacts.
When `Full Release Validation` completes and
`OPENCLAW_RELEASES_PRIVATE_DISPATCH_TOKEN` is configured in the public repo, it
requests the private `OpenClaw Release Evidence From Full Validation` workflow.
That private workflow reads the parent full-validation run, extracts the child
CI/release-checks/Telegram run ids from the parent logs, and opens the evidence
PR automatically. If the token is absent or the run predates this wiring, trigger
that private workflow manually with the full-validation run id.
### Release Checks
`OpenClaw Release Checks` (`openclaw-release-checks.yml`) is the release child
workflow. It is broader than normal CI but narrower than the umbrella because it
does not dispatch the separate full normal CI child. It runs Package Acceptance
with artifact-native delta lanes and `telegram_mode=mock-openai`, so the release
package tarball also goes through offline plugin proof, bundled-channel compat,
and Telegram package QA. The Docker release-path chunks cover the overlapping
package/update/plugin lanes. Use it when release-path validation is needed
without rerunning the entire umbrella.
```bash
gh workflow run openclaw-release-checks.yml \
--repo openclaw/openclaw \
--ref main \
-f ref=<branch-or-sha> \
-f provider=openai \
-f mode=both \
-f release_profile=stable \
-f rerun_group=all
```
Release-check rerun groups are `all`, `install-smoke`, `cross-os`, `live-e2e`,
`package`, `qa`, `qa-parity`, and `qa-live`.
`OpenClaw Release Checks` uses the trusted workflow ref to resolve the selected
ref once as `release-package-under-test` and passes that artifact into cross-OS
release checks, release-path Docker live/E2E checks, and Package Acceptance.
When `Full Release Validation` dispatches release checks, it passes the requested
branch/tag plus an `expected_sha` so branch/tag refs resolve through the fast
remote-ref path while the package and QA jobs still validate the exact SHA.
The full install-smoke child is split on purpose: one job prepares or reuses the
target-SHA GHCR root Dockerfile smoke image, QR package install runs in its own
job, root Dockerfile/gateway smokes pull the prepared image, and installer/Bun
smokes pull the same image while building only their small installer images.
If install-smoke gets slow again, first check whether the root image was reused
or rebuilt before adding/removing coverage.
The full-profile native live media shards use the prebuilt
`ghcr.io/openclaw/openclaw-live-media-runner:ubuntu-24.04` container so
`ffmpeg`/`ffprobe` are already present. If those jobs suddenly spend minutes in
dependency setup again, first check the `Live Media Runner Image` workflow and
the `Verify preinstalled live media dependencies` step before assuming the media
tests themselves slowed down.
The release Docker path intentionally shards the plugin/runtime tail. The
workflow uses `plugins-runtime-plugins`, `plugins-runtime-services`, and
`plugins-runtime-install-a` through `plugins-runtime-install-d`; aggregate
aliases such as `plugins-runtime-core`, `plugins-runtime`, and
`plugins-integrations` remain for manual reruns.
The release QA parity box is internally split into candidate and baseline lane
jobs, followed by a report job that downloads both artifacts and runs
`pnpm openclaw qa parity-report`. For parity failures, inspect the failed lane
first; inspect the report job when both lane summaries exist but the comparison
fails.
### QA Lab Matrix Profiles
`pnpm openclaw qa matrix` defaults to `--profile all`. Do not assume the CLI
default is the fast release path. Use explicit profiles:
- `--profile fast`: release-critical Matrix transport contract; add
`--fail-fast` only when the target CLI supports it
- `--profile transport|media|e2ee-smoke|e2ee-deep|e2ee-cli`: sharded full
Matrix proof
- `OPENCLAW_QA_MATRIX_NO_REPLY_WINDOW_MS=3000`: CI-friendly no-reply quiet
window when paired with fast or sharded gates
`QA-Lab - All Lanes` uses explicit fast Matrix on scheduled runs; manual
dispatch keeps `matrix_profile=all` as the default and always shards that full
Matrix selection. `OpenClaw Release Checks` uses explicit fast Matrix; run the
all-lanes workflow when release investigation needs full Matrix media/E2EE
inventory.
### Reusable Live/E2E Checks
`OpenClaw Live And E2E Checks (Reusable)`
(`openclaw-live-and-e2e-checks-reusable.yml`) is the preferred entry point for
targeted live, Docker, model, and E2E proof. Inputs let you turn off unrelated
lanes:
```bash
gh workflow run openclaw-live-and-e2e-checks-reusable.yml \
--repo openclaw/openclaw \
--ref main \
-f ref=<sha> \
-f include_repo_e2e=false \
-f include_release_path_suites=false \
-f include_openwebui=false \
-f include_live_suites=true \
-f live_models_only=true \
-f live_model_providers=fireworks
```
Useful knobs:
- `docker_lanes='<lane[,lane]>'`: run selected Docker scheduler lanes against
prepared artifacts instead of the release chunk matrix. Multiple selected
lanes fan out as parallel targeted Docker jobs after one shared package/image
preparation step.
- `include_live_suites=false`: skip live/provider suites when testing Docker
scheduler or release packaging only.
- `live_models_only=true`: run only Docker live model coverage.
- `live_model_providers=fireworks` (or comma/space separated providers): run one
targeted Docker live model job instead of the full provider matrix.
- blank `live_model_providers`: run the full live-model provider matrix.
Release-path Docker chunks are currently `core`, `package-update-openai`,
`package-update-anthropic`, `package-update-core`,
`plugins-runtime-plugins`, `plugins-runtime-services`,
`plugins-runtime-install-a`, `plugins-runtime-install-b`,
`plugins-runtime-install-c`, `plugins-runtime-install-d`,
`bundled-channels-core`, `bundled-channels-update-a`,
`bundled-channels-update-b`, and `bundled-channels-contracts`. The aggregate
`bundled-channels`, `plugins-runtime-core`, `plugins-runtime`, and
`plugins-integrations` chunks remain valid for manual one-shot reruns, but
release checks use the split chunks.
When live suites are enabled, the workflow shards broad native `pnpm test:live`
coverage through `scripts/test-live-shard.mjs` instead of one serial `live-all`
job:
- `native-live-src-agents`
- `native-live-src-gateway-core`
- `native-live-src-gateway-profiles` (release CI runs this with provider
filters such as `OPENCLAW_LIVE_GATEWAY_PROVIDERS=anthropic`)
- `native-live-src-gateway-backends`
- `native-live-test`
- `native-live-extensions-a-k`
- `native-live-extensions-l-n`
- `native-live-extensions-openai`
- `native-live-extensions-o-z`
- `native-live-extensions-o-z-other`
- `native-live-extensions-xai`
- `native-live-extensions-media`
- `native-live-extensions-media-audio`
- `native-live-extensions-media-music`
- `native-live-extensions-media-music-google`
- `native-live-extensions-media-music-minimax`
- `native-live-extensions-media-video`
Use `node scripts/test-live-shard.mjs <shard> --list` to see the exact files
before rerunning a failed native live shard. The aggregate `o-z` and `media`
shards remain useful locally; release CI uses the smaller provider/media shards
so one live-provider flake does not force a broad native live rerun.
For model-list or provider-selection fixes, use `live_models_only=true` plus the
specific `live_model_providers` allowlist. Confirm logs show the expected
`OPENCLAW_LIVE_PROVIDERS` and selected model ids before declaring proof.
## Docker
Docker is expensive. First inspect the scheduler without running Docker:
@@ -400,143 +131,16 @@ Multiple lanes are allowed:
docker_lanes: install-e2e bundled-channel-update-acpx
```
That skips the release chunk matrix and runs one targeted Docker job against the
prepared GHCR images and the selected package artifact. Rerun commands
generated inside GitHub artifacts include `package_artifact_run_id`,
`package_artifact_name`, `docker_e2e_bare_image`, and
`docker_e2e_functional_image` when available, so failed lanes can reuse the
exact tarball and prepared images from the failed run. When the fix changes
package contents, omit those reuse inputs so the workflow packs a new tarball.
That skips the three chunk matrix and runs one targeted Docker job against the
prepared GHCR images and a fresh OpenClaw npm tarball for the selected ref.
Reruns usually need that new tarball because the fix being tested changed the
package contents even if the SHA-tagged GHCR Docker image can be reused.
Live-only targeted reruns skip the E2E images and build only the live-test
image. Release-path normal mode fans out into smaller Docker chunk jobs:
image. Release-path normal mode remains max three Docker chunk jobs:
- `core`
- `package-update-openai`
- `package-update-anthropic`
- `package-update-core`
- `plugins-runtime-plugins`
- `plugins-runtime-services`
- `plugins-runtime-install-a`
- `plugins-runtime-install-b`
- `plugins-runtime-install-c`
- `plugins-runtime-install-d`
- `bundled-channels`
OpenWebUI is folded into `plugins-runtime-services` for full release-path
coverage and keeps a standalone `openwebui` chunk only for OpenWebUI-only
dispatches. The legacy `package-update`, `plugins-runtime-core`,
`plugins-runtime`, and `plugins-integrations` chunks still work as aggregate
aliases for manual reruns, but the release workflow uses the split chunks so
provider installer checks, plugin runtime checks, bundled plugin
install/uninstall shards, and bundled-channel checks can run on separate
machines. The bundled-channel runtime-dependency coverage
inside `bundled-channels`
uses the split `bundled-channel-*` and `bundled-channel-update-*` lanes rather
than the serial `bundled-channel-deps` lane, so failures produce cheap targeted
reruns for the exact channel/update scenario. The bundled plugin
install/uninstall sweep is also split into
`bundled-plugin-install-uninstall-0` through
`bundled-plugin-install-uninstall-7`; selecting the legacy
`bundled-plugin-install-uninstall` lane expands to all eight shards.
## Package Acceptance
Use the manual `Package Acceptance` workflow when the question is "does this
installable package work as a product?" rather than "does this source diff pass
Vitest?"
In release validation, treat Package Acceptance as the package-candidate shard
inside the larger release umbrella, not as a competing full-test path. Full
Release Validation and private release gauntlets should call Package Acceptance
for tarball resolution, Docker product/package proof, and optional Telegram QA
against the same resolved `package-under-test` artifact; keep orchestration,
secret policy, blocking/advisory status, and evidence rollup in the caller.
Good defaults:
```bash
gh workflow run package-acceptance.yml --ref main \
-f source=npm \
-f workflow_ref=main \
-f package_spec=openclaw@beta \
-f suite_profile=product \
-f telegram_mode=mock-openai
```
Npm candidate selection:
- Resolve the registry immediately before dispatch:
`npm view openclaw dist-tags --json --prefer-online --cache /tmp/openclaw-npm-cache-verify-$$`
and `npm view openclaw@beta version dist.tarball dist.integrity --json --prefer-online --cache /tmp/openclaw-npm-cache-verify-$$`.
- If Peter asks for "latest beta", use `source=npm` with
`package_spec=openclaw@beta`, then record the resolved version from `npm view`
or the workflow summary.
- For reruns, release proof, or comparing one known package, prefer the exact
immutable spec: `package_spec=openclaw@YYYY.M.D-beta.N` or
`package_spec=openclaw@YYYY.M.D`.
- For stable package proof, use `package_spec=openclaw@latest` only when the
question is explicitly the current stable dist-tag; otherwise pin the exact
version.
- `source=npm` only accepts registry specs for `openclaw@beta`,
`openclaw@latest`, or exact OpenClaw release versions. Do not pass semver
ranges, git refs, file paths, tarball URLs, or plugin package names there.
- If the candidate is a tarball URL, use `source=url` with `package_sha256`. If
it is an Actions tarball artifact, use `source=artifact`. If it is an
unpublished source candidate, use `source=ref` with a trusted ref or SHA.
- Package acceptance tests exactly the selected package candidate. Do not apply
`openclaw update --channel beta` fallback semantics here; if `beta` is absent,
stale, older than `latest`, or points at a broken tarball, report that tag
state instead of silently testing `latest`.
Profiles:
- `smoke`: quick confidence that the tarball installs, can onboard a channel,
can run an agent turn, and basic gateway/config lanes work.
- `package`: release-package contract. Adds installer/update, doctor install
switching, bundled plugin runtime deps, plugin install/update, and package
repair lanes. This is the default native replacement for most Parallels
package/update coverage.
- `product`: package profile plus broader product surfaces: MCP channels,
cron/subagent cleanup, OpenAI web search, and OpenWebUI.
- `full`: split Docker release-path chunks with OpenWebUI.
- `custom`: exact `docker_lanes` list for a focused rerun.
Candidate sources:
- `source=npm`: `openclaw@beta`, `openclaw@latest`, or an exact release version.
- `source=ref`: pack `package_ref` using the trusted `workflow_ref` harness.
This intentionally separates old package commits from new workflow/test code.
- `source=url`: HTTPS `.tgz` plus required `package_sha256`.
- `source=artifact`: download one `.tgz` from `artifact_run_id`/`artifact_name`.
Ref model:
- `gh workflow run ... --ref <workflow-ref>` selects the workflow file revision
GitHub executes.
- `workflow_ref` is the trusted harness/script ref passed to reusable Docker
E2E.
- `package_ref` is the source ref to build when `source=ref`. It can be an
older branch/tag/SHA as long as it is reachable from an OpenClaw branch or
release tag.
Example: run latest package acceptance harness against an older trusted commit:
```bash
gh workflow run package-acceptance.yml --ref main \
-f workflow_ref=main \
-f source=ref \
-f package_ref=<branch-or-sha> \
-f suite_profile=package \
-f telegram_mode=mock-openai
```
Use `telegram_mode=mock-openai` or `telegram_mode=live-frontier` when the same
resolved `package-under-test` tarball should also run through the Telegram QA
workflow in the `qa-live-shared` environment. The standalone Telegram workflow
still accepts a published npm spec for post-publish checks, but Package
Acceptance passes the resolved artifact for `source=npm`, `ref`, `url`, and
`artifact`. Use `telegram_mode=none` only when intentionally skipping Telegram
credentialed package proof for a focused rerun.
- `package-update`
- `plugins-integrations`
Docker E2E images never copy repo sources as the app under test: the bare image
is a Node/Git runner, and the functional image installs the same prebuilt npm
@@ -583,7 +187,7 @@ gh workflow run openclaw-live-and-e2e-checks-reusable.yml \
That path still runs the prepare job, so it creates a new tarball for `<sha>`.
If the SHA-tagged GHCR bare/functional image already exists, CI skips rebuilding
that image and only uploads the fresh package artifact before the targeted lane
job. Do not rerun the full release path unless the failed lane list
job. Do not rerun the full three-chunk release path unless the failed lane list
or touched surface really requires it.
## Docker Expected Timings
@@ -600,7 +204,7 @@ these rough bands:
`session-runtime-context` ~20s, `gateway-network` ~34s, `qr` ~44s.
- Medium deterministic lanes, ~1-5 minutes:
`npm-onboard-channel-agent` ~96s, `openai-image-auth` ~99s,
bundled channel/update lanes usually ~90-300s when split, `openwebui` ~225s,
bundled channel/update lanes usually ~90-300s, `openwebui` ~225s,
`mcp-channels` ~274s.
- Heavy deterministic lanes, ~6-10 minutes:
`bundled-channel-root-owned` ~429s,

View File

@@ -1,6 +1,6 @@
---
name: tag-duplicate-prs-issues
description: Use gitcrawl to search duplicate OpenClaw PRs/issues, group related work in prtags, and sync duplicate state to GitHub.
description: Search duplicate OpenClaw PRs/issues, group related work in prtags, and sync duplicate state to GitHub.
---
# Tag Duplicate PRs and Issues
@@ -12,25 +12,43 @@ It is not for reviewing the implementation quality of a PR.
## Required Setup
Do not write duplicate groups or annotations until this setup is complete.
Read-only discovery can still proceed with `gitcrawl` and live `gh`.
Do not start duplicate triage until this setup is complete.
### Companion Skills
### Install the companion skills
Use `$gitcrawl` first for local candidate discovery.
Use the `prtags` skill from the `prtags` repo at `skills/prtags/SKILL.md` when it is available.
Install these skills first because they teach the agent how to use the two main CLIs correctly:
- `ghreplica` skill from the `ghreplica` repo at `skills/ghreplica/SKILL.md`
- `prtags` skill from the `prtags` repo at `skills/prtags/SKILL.md`
This skill assumes those two skills are available and can be used during the same run.
### Install the CLIs
Install `prtags` from its latest GitHub release.
Install `ghreplica` and `prtags` from their latest GitHub releases.
Do not rely on an old local build unless the maintainer explicitly wants to test unreleased behavior.
`ghreplica` CLI install path:
```bash
curl -fsSL https://raw.githubusercontent.com/dutifuldev/ghreplica/main/scripts/install-ghr.sh | bash -s -- --bin-dir "$HOME/.local/bin"
```
`prtags` CLI install path:
```bash
curl -fsSL https://raw.githubusercontent.com/dutifuldev/prtags/main/scripts/install-prtags.sh | bash -s -- --bin-dir "$HOME/.local/bin"
```
Use the `pr-search-cli` project with `uvx`.
The command itself is `pr-search`.
Do not require a permanent install unless the maintainer explicitly wants one.
```bash
uvx --from pr-search-cli pr-search status
uvx --from pr-search-cli pr-search code similar 67144
```
### Authenticate prtags
`prtags` should be logged in with the maintainer's own GitHub account through OAuth device flow.
@@ -48,15 +66,20 @@ The expected outcome is that `prtags` stores the logged-in maintainer identity l
Do not require an up-front preflight before starting the workflow.
Proceed with the normal steps until you actually need a tool or account state.
As soon as you discover that `prtags` is missing or not logged in at the write step, stop immediately.
Do not continue in a partial write mode after that point.
As soon as you discover that a required CLI is missing or `prtags` is not logged in, stop immediately.
Do not continue in a partial mode after that point.
If `prtags` is missing, ask the user to run:
If `ghr` is missing, ask the user to run the `ghreplica` install command.
If `prtags` is missing, ask the user to run both CLI install commands:
```bash
curl -fsSL https://raw.githubusercontent.com/dutifuldev/ghreplica/main/scripts/install-ghr.sh | bash -s -- --bin-dir "$HOME/.local/bin"
curl -fsSL https://raw.githubusercontent.com/dutifuldev/prtags/main/scripts/install-prtags.sh | bash -s -- --bin-dir "$HOME/.local/bin"
```
If `uvx --from pr-search-cli pr-search ...` fails because `uvx` or the `pr-search` launcher is not available, ask the user to make that command work before continuing.
If `prtags auth status` shows that the user is not logged in, ask the user to run:
```bash
@@ -67,19 +90,19 @@ Resume only after the missing tool or login state has been fixed.
## Read-Path Default
For candidate discovery in this workflow, use `gitcrawl` first.
Treat it as the local history and clustering layer for related issues, duplicate attempts, and closed threads.
For read-only GitHub operations in this workflow, use `ghr` as the default CLI.
Treat it as a drop-in replacement for the `gh` read operations you would normally use for PRs, issues, comments, reviews, and duplicate-search evidence.
Use live `gh` or `gh api` for the target thread and for any candidate before making an actionable judgment.
Use live GitHub when `gitcrawl` is missing or stale for a concrete reason, such as:
Only fall back to `gh` when `ghr` is failing for a concrete reason, such as:
- the target or candidate is not present yet
- the local data is clearly stale or incomplete for the decision you need to make
- `gitcrawl` errors, times out, or lacks the needed neighbor/search data
- the mirrored object is not present yet
- the mirror data is clearly stale or incomplete for the decision you need to make
- the `ghr` command errors, times out, or does not expose the specific read you need
When you fall back to live GitHub search, note that you did so and why.
When you fall back to `gh`, note that you did so and why.
If a later `prtags` target-level write fails because its own mirror has not caught up, stop and report that the curation backend is missing the target object instead of forcing a fallback write.
If `ghr` is missing a fresh PR or issue but `gh` can read it, you may use `gh` for the read-side judgment.
If a later `prtags` target-level write fails because the same object is still missing from `ghreplica`, stop and report that the mirror has not caught up yet instead of forcing the write.
## Goal
@@ -95,12 +118,14 @@ For each target PR or issue:
Use the tools with these boundaries:
- `gitcrawl` is candidate generation and historical context
- use it first for local title/body search, neighbors, clusters, and closed-thread discovery
- treat every candidate as a lead until live GitHub confirms it
- `gh` is live GitHub truth
- use it for target state, body, comments, reviews, files, linked issues, and current open/closed/merged status
- use `gh search` only when `gitcrawl` is stale, missing data, or cannot express the needed query
- `ghreplica` is the raw evidence source
- use `ghr` first for normal GitHub read operations in this workflow
- use it for title/body/comment search, related PRs, overlapping files, overlapping ranges, and current PR or issue status
- resort to `gh` only when `ghr` cannot provide the needed read cleanly
- `pr-search-cli` is candidate generation and ranking
- use it to suggest likely duplicate PRs or issue-cluster context
- do not treat it as final truth
- do not create or expand a duplicate group only because `pr-search-cli` put multiple PRs in the same issue or duplicate cluster
- `prtags` is the maintainer curation layer
- use it to create or reuse one duplicate group
- use it to save the duplicate status, confidence, rationale, and group summary
@@ -157,7 +182,7 @@ Examples:
## Evidence Checklist
Before declaring a duplicate, gather evidence from at least two categories.
`gitcrawl` neighbors, search hits, and cluster membership count as candidate generation, not as enough proof by themselves.
Same-issue or same-cluster output from `pr-search-cli` counts only as candidate generation, not as one of the required proof categories by itself.
For PRs:
@@ -180,18 +205,21 @@ If you only have wording similarity, that is not enough.
## Step 1: Read The Target
Start by reading the target itself.
Use live GitHub for current target state.
Use `ghr` first for this step even if you would normally reach for `gh`.
For a PR:
```bash
gh pr view <number> --json number,title,state,mergedAt,body,closingIssuesReferences,files,comments,reviews,statusCheckRollup
ghr pr view -R openclaw/openclaw <number> --comments
ghr pr reviews -R openclaw/openclaw <number>
ghr pr comments -R openclaw/openclaw <number>
```
For an issue:
```bash
gh issue view <number> --json number,title,state,body,comments,closedAt
ghr issue view -R openclaw/openclaw <number> --comments
ghr issue comments -R openclaw/openclaw <number>
```
Record:
@@ -204,56 +232,74 @@ Record:
- whether it is open, closed, or merged
- whether there is already a likely duplicate thread mentioned by humans
## Step 2: Search Broadly With Gitcrawl
## Step 2: Search Broadly With ghreplica
Use `gitcrawl` first because it is the local OpenClaw history and clustering source.
Do not switch to broad live GitHub search unless `gitcrawl` is missing data, stale, or failing.
Use `ghreplica` first because it is the most direct evidence source.
Do not switch to `gh` for ordinary reads unless `ghr` is missing data or failing.
Start with the target and nearby threads:
### PR duplicate search
Run all of these when the target is a PR:
```bash
gitcrawl threads openclaw/openclaw --numbers <issue-or-pr-number> --include-closed --json
gitcrawl neighbors openclaw/openclaw --number <issue-or-pr-number> --limit 20 --json
ghr search related-prs -R openclaw/openclaw <pr-number> --mode path_overlap --state all
ghr search related-prs -R openclaw/openclaw <pr-number> --mode range_overlap --state all
ghr search mentions -R openclaw/openclaw --query "<key phrase from title or body>" --mode fts --scope pull_requests --state all
ghr search mentions -R openclaw/openclaw --query "<subsystem or error phrase>" --mode fts --scope issues --state all
```
Then search key phrases and subsystem terms:
Use `prs-by-paths` or `prs-by-ranges` when the likely duplicate surface is already known:
```bash
gitcrawl search openclaw/openclaw --query "<key phrase from title or body>" --mode hybrid --limit 20 --json
gitcrawl search openclaw/openclaw --query "<subsystem or error phrase>" --mode hybrid --limit 20 --json
ghr search prs-by-paths -R openclaw/openclaw --path src/example.ts --state all
ghr search prs-by-ranges -R openclaw/openclaw --path src/example.ts --start 20 --end 80 --state all
```
Inspect likely clusters:
### Issue duplicate search
`ghreplica` does not have a special issue-to-issue “related issues” command.
For issues, search mirrored text and linked PR context instead.
Run targeted text searches:
```bash
gitcrawl cluster-detail openclaw/openclaw --id <cluster-id> --member-limit 20 --body-chars 280 --json
ghr search mentions -R openclaw/openclaw --query "<issue title phrase>" --mode fts --scope issues --state all
ghr search mentions -R openclaw/openclaw --query "<error message or symptom>" --mode fts --scope issues --state all
ghr search mentions -R openclaw/openclaw --query "<subsystem phrase>" --mode fts --scope pull_requests --state all
```
For PRs, verify likely code overlap with live file data:
Then inspect the candidate PRs or issues those searches uncover.
## Step 3: Use pr-search-cli As A Hint Layer
Use `pr-search-cli` after `ghreplica`.
It is good at surfacing candidates quickly, but it is not the final decision-maker.
Run it through the `pr-search` command.
For a PR:
```bash
gh pr view <candidate-pr> --json number,title,state,mergedAt,files,body,comments,reviews
uvx --from pr-search-cli pr-search -R openclaw/openclaw code similar <pr-number>
uvx --from pr-search-cli pr-search -R openclaw/openclaw code clusters for-pr <pr-number>
uvx --from pr-search-cli pr-search -R openclaw/openclaw issues for-pr <pr-number>
uvx --from pr-search-cli pr-search -R openclaw/openclaw issues duplicate-prs
```
For issues, verify likely duplicate issue state and comments live:
Interpretation:
```bash
gh issue view <candidate-issue> --json number,title,state,body,comments,closedAt
```
- `code similar` suggests PRs with similar change shape
- `code clusters for-pr` shows the PRs nearby code cluster
- `issues for-pr` shows which issue clusters the PR appears to belong to
- `issues duplicate-prs` is useful for spotting already-known duplicate PR patterns
## Step 3: Use Live GitHub Search For Gaps
Treat every `pr-search-cli` result as a hint to investigate, not as enough evidence to create or widen a duplicate group.
Multiple PRs can share the same issue or issue cluster while still taking meaningfully different fix paths.
Use targeted live GitHub search after `gitcrawl` when:
For an issue:
- the target is too new for the local store
- comments or reviews matter and the local store lacks them
- the exact phrase did not appear in local results but the issue/PR is current enough that GitHub should know it
```bash
gh search prs --repo openclaw/openclaw --match title,body --limit 50 -- "<key phrase>"
gh search issues --repo openclaw/openclaw --match title,body --limit 50 -- "<key phrase>"
gh search issues --repo openclaw/openclaw --match comments --limit 50 -- "<error or maintainer phrase>"
```
- use `ghreplica` first to find candidate PRs or issue wording
- if the issue has linked PRs or a likely implementation PR, run `pr-search-cli` on those PRs
- treat issue-cluster output as supporting context, not as enough by itself to call the issue a duplicate
## Step 4: Decide The Outcome
@@ -298,7 +344,7 @@ Reuse an existing group when:
- it already contains clearly related members
- adding the target would keep the group coherent
Do not widen an existing group just because `gitcrawl` placed several PRs or issues near each other.
Do not widen an existing group just because `pr-search-cli` placed several PRs under the same issue or duplicate cluster.
Confirm that the actual implementation path and maintainer intent still match before adding the new member.
Create a new group only when no existing group clearly fits.
@@ -377,8 +423,8 @@ prtags annotation group set <group-id> \
When the evidence is incomplete, set `duplicate_status=candidate` and lower the confidence.
If a per-PR or per-issue annotation write fails because `prtags` cannot resolve the target, do not force a fallback write path.
Keep the group state you were able to write, report that the curation backend is still missing the target object, and defer the target-level annotation until `prtags` catches up.
If a per-PR or per-issue annotation write fails because `prtags` cannot resolve the target through `ghreplica`, do not force a fallback write path.
Keep the group state you were able to write, report that the mirror is still missing the target object, and defer the target-level annotation until `ghreplica` catches up.
## Step 8: Let prtags Sync The Group Comment

View File

@@ -1,4 +1,4 @@
interface:
display_name: "Tag Duplicate PRs and Issues"
short_description: "Find duplicate PRs and issues with gitcrawl, group them in prtags, and let prtags sync the GitHub comment"
default_prompt: "Use $tag-duplicate-prs-issues to decide whether an OpenClaw PR or issue is a duplicate, gather candidates with gitcrawl, verify live state with GitHub, group related items in prtags, and save the duplicate judgment."
short_description: "Find duplicate PRs and issues, group them in prtags, and let prtags sync the GitHub comment"
default_prompt: "Use $tag-duplicate-prs-issues to decide whether an OpenClaw PR or issue is a duplicate, gather evidence with ghreplica and pr-search-cli, group related items in prtags, and save the duplicate judgment."

View File

@@ -1,41 +0,0 @@
profile: openclaw-check
provider: aws
class: beast
capacity:
market: spot
strategy: most-available
fallback: on-demand-after-120s
regions:
- eu-west-1
actions:
workflow: .github/workflows/crabbox-hydrate.yml
job: hydrate
ref: main
runnerLabels:
- crabbox
- openclaw
runnerVersion: latest
ephemeral: true
aws:
region: eu-west-1
rootGB: 400
sync:
delete: true
checksum: false
gitSeed: true
fingerprint: true
baseRef: main
exclude:
- .artifacts
- .codex
- .DS_Store
- playwright-report
- test-results
env:
allow:
- CI
- NODE_OPTIONS
- OPENCLAW_*
ssh:
user: crabbox
port: "2222"

84
.github/CODEOWNERS vendored
View File

@@ -2,51 +2,49 @@
/.github/CODEOWNERS @steipete
# WARNING: GitHub CODEOWNERS uses last-match-wins semantics.
# If you add overlapping rules below the secops block, include @openclaw/openclaw-secops
# If you add overlapping rules below the secops block, include @openclaw/secops
# on those entries too or you can silently remove required secops review.
# Security-sensitive code, config, and docs require secops review.
/SECURITY.md @openclaw/openclaw-secops
/.github/dependabot.yml @openclaw/openclaw-secops
/.github/codeql/ @openclaw/openclaw-secops
/.github/workflows/codeql.yml @openclaw/openclaw-secops
/.github/workflows/codeql-android-critical-security.yml @openclaw/openclaw-secops
/.github/workflows/codeql-critical-quality.yml @openclaw/openclaw-secops
/src/security/ @openclaw/openclaw-secops
/src/secrets/ @openclaw/openclaw-secops
/src/config/*secret*.ts @openclaw/openclaw-secops
/src/config/**/*secret*.ts @openclaw/openclaw-secops
/src/gateway/*auth*.ts @openclaw/openclaw-secops
/src/gateway/**/*auth*.ts @openclaw/openclaw-secops
/src/gateway/*secret*.ts @openclaw/openclaw-secops
/src/gateway/**/*secret*.ts @openclaw/openclaw-secops
/src/gateway/security-path*.ts @openclaw/openclaw-secops
/src/gateway/resolve-configured-secret-input-string*.ts @openclaw/openclaw-secops
/src/gateway/protocol/**/*secret*.ts @openclaw/openclaw-secops
/src/gateway/server-methods/secrets*.ts @openclaw/openclaw-secops
/src/agents/*auth*.ts @openclaw/openclaw-secops
/src/agents/**/*auth*.ts @openclaw/openclaw-secops
/src/agents/auth-profiles*.ts @openclaw/openclaw-secops
/src/agents/auth-health*.ts @openclaw/openclaw-secops
/src/agents/auth-profiles/ @openclaw/openclaw-secops
/src/agents/sandbox.ts @openclaw/openclaw-secops
/src/agents/sandbox-*.ts @openclaw/openclaw-secops
/src/agents/sandbox/ @openclaw/openclaw-secops
/src/infra/secret-file*.ts @openclaw/openclaw-secops
/src/cron/stagger.ts @openclaw/openclaw-secops
/src/cron/service/jobs.ts @openclaw/openclaw-secops
/docs/security/ @openclaw/openclaw-secops
/docs/gateway/authentication.md @openclaw/openclaw-secops
/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md @openclaw/openclaw-secops
/docs/gateway/sandboxing.md @openclaw/openclaw-secops
/docs/gateway/secrets-plan-contract.md @openclaw/openclaw-secops
/docs/gateway/secrets.md @openclaw/openclaw-secops
/docs/gateway/security/ @openclaw/openclaw-secops
/docs/cli/approvals.md @openclaw/openclaw-secops
/docs/cli/sandbox.md @openclaw/openclaw-secops
/docs/cli/security.md @openclaw/openclaw-secops
/docs/cli/secrets.md @openclaw/openclaw-secops
/docs/reference/secretref-credential-surface.md @openclaw/openclaw-secops
/docs/reference/secretref-user-supplied-credentials-matrix.json @openclaw/openclaw-secops
/SECURITY.md @openclaw/secops
/.github/dependabot.yml @openclaw/secops
/.github/codeql/ @openclaw/secops
/.github/workflows/codeql.yml @openclaw/secops
/src/security/ @openclaw/secops
/src/secrets/ @openclaw/secops
/src/config/*secret*.ts @openclaw/secops
/src/config/**/*secret*.ts @openclaw/secops
/src/gateway/*auth*.ts @openclaw/secops
/src/gateway/**/*auth*.ts @openclaw/secops
/src/gateway/*secret*.ts @openclaw/secops
/src/gateway/**/*secret*.ts @openclaw/secops
/src/gateway/security-path*.ts @openclaw/secops
/src/gateway/resolve-configured-secret-input-string*.ts @openclaw/secops
/src/gateway/protocol/**/*secret*.ts @openclaw/secops
/src/gateway/server-methods/secrets*.ts @openclaw/secops
/src/agents/*auth*.ts @openclaw/secops
/src/agents/**/*auth*.ts @openclaw/secops
/src/agents/auth-profiles*.ts @openclaw/secops
/src/agents/auth-health*.ts @openclaw/secops
/src/agents/auth-profiles/ @openclaw/secops
/src/agents/sandbox.ts @openclaw/secops
/src/agents/sandbox-*.ts @openclaw/secops
/src/agents/sandbox/ @openclaw/secops
/src/infra/secret-file*.ts @openclaw/secops
/src/cron/stagger.ts @openclaw/secops
/src/cron/service/jobs.ts @openclaw/secops
/docs/security/ @openclaw/secops
/docs/gateway/authentication.md @openclaw/secops
/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md @openclaw/secops
/docs/gateway/sandboxing.md @openclaw/secops
/docs/gateway/secrets-plan-contract.md @openclaw/secops
/docs/gateway/secrets.md @openclaw/secops
/docs/gateway/security/ @openclaw/secops
/docs/cli/approvals.md @openclaw/secops
/docs/cli/sandbox.md @openclaw/secops
/docs/cli/security.md @openclaw/secops
/docs/cli/secrets.md @openclaw/secops
/docs/reference/secretref-credential-surface.md @openclaw/secops
/docs/reference/secretref-user-supplied-credentials-matrix.json @openclaw/secops
# Release workflow and its supporting release-path checks.
/.github/workflows/openclaw-npm-release.yml @openclaw/openclaw-release-managers

View File

@@ -4,7 +4,6 @@
self-hosted-runner:
labels:
# Blacksmith CI runners
- blacksmith-4vcpu-ubuntu-2404
- blacksmith-8vcpu-ubuntu-2404
- blacksmith-8vcpu-windows-2025
- blacksmith-16vcpu-ubuntu-2404

View File

@@ -26,10 +26,6 @@ inputs:
description: Whether to download/pull artifacts required by the plan.
required: false
default: "true"
package-artifact-name:
description: Workflow artifact name containing openclaw-current.tgz.
required: false
default: docker-e2e-package
outputs:
credentials:
description: Comma-separated credential groups required by selected lanes.
@@ -112,7 +108,7 @@ runs:
if: inputs.hydrate-artifacts == 'true' && steps.plan.outputs.needs_package == '1'
uses: actions/download-artifact@v8
with:
name: ${{ inputs.package-artifact-name }}
name: docker-e2e-package
path: .artifacts/docker-e2e-package
- name: Pull shared bare Docker E2E image

View File

@@ -90,11 +90,9 @@ runs:
install_args=(
install
--prefer-offline
--ignore-scripts=false
--config.engine-strict=false
--config.enable-pre-post-scripts=true
--config.side-effects-cache=true
)
if [ -n "$LOCKFILE_FLAG" ]; then
install_args+=("$LOCKFILE_FLAG")

View File

@@ -1,21 +0,0 @@
name: openclaw-codeql-actions-critical-security
disable-default-queries: true
queries:
- uses: security-extended
query-filters:
- include:
precision:
- high
- very-high
tags contain: security
security-severity: /([7-9]|10)\.(\d)+/
paths:
- .github/actions
- .github/workflows
paths-ignore:
- .github/workflows/stale.yml

View File

@@ -1,53 +0,0 @@
name: openclaw-codeql-agent-runtime-boundary-critical-quality
disable-default-queries: true
queries:
- uses: security-and-quality
query-filters:
- include:
problem.severity:
- error
- exclude:
tags:
- security
paths:
- src/acp/control-plane
- src/agents/command
- src/agents/cli-runner
- src/agents/pi-embedded-runner
- src/agents/tools
- src/agents/*completion*.ts
- src/agents/*transport*.ts
- src/agents/model-*.ts
- src/agents/openclaw-tools*.ts
- src/agents/provider-*.ts
- src/agents/session*.ts
- src/agents/tool-call*.ts
- src/auto-reply/reply/agent-runner*.ts
- src/auto-reply/reply/commands*.ts
- src/auto-reply/reply/directive-handling*.ts
- src/auto-reply/reply/dispatch-*.ts
- src/auto-reply/reply/get-reply-run*.ts
- src/auto-reply/reply/provider-dispatcher*.ts
- src/auto-reply/reply/queue*.ts
- src/auto-reply/reply/reply-run-registry*.ts
- src/auto-reply/reply/session*.ts
paths-ignore:
- "**/node_modules"
- "**/coverage"
- "**/*.generated.ts"
- "**/*.bundle.js"
- "**/*-runtime.js"
- "**/*.test.ts"
- "**/*.test.tsx"
- "**/*.e2e.test.ts"
- "**/*.e2e.test.tsx"
- "**/*test-support*"
- "**/*test-helper*"
- "**/*mock*"
- "**/*fixture*"
- "**/*bench*"

View File

@@ -1,30 +0,0 @@
name: openclaw-codeql-android-critical-security
disable-default-queries: true
queries:
- uses: security-extended
query-filters:
# Android canvas intentionally runs trusted A2UI JavaScript; keep this profile focused on exploitable WebView edges.
- exclude:
id: java/android/websettings-javascript-enabled
# Gateway TLS already pins verified certificate SHA-256 fingerprints. OkHttp CertificatePinner pins SPKI hashes,
# so this query is noisy for OpenClaw's TOFU/local-gateway trust model and does not belong in the critical profile.
- exclude:
id: java/android/missing-certificate-pinning
paths:
- apps/android/app/src/main
paths-ignore:
- "**/.gradle"
- "**/build"
- "**/node_modules"
- "**/coverage"
- "**/*.generated.*"
- "**/*Test.kt"
- "**/*Test.java"
- "**/*Benchmark.kt"
- apps/android/app/src/test
- apps/android/benchmark

View File

@@ -1,56 +0,0 @@
name: openclaw-codeql-channel-runtime-boundary-critical-quality
disable-default-queries: true
queries:
- uses: security-and-quality
query-filters:
- include:
problem.severity:
- error
- exclude:
tags:
- security
paths:
- extensions/bluebubbles/src
- extensions/discord/src
- extensions/feishu/src
- extensions/googlechat/src
- extensions/imessage/src
- extensions/irc/src
- extensions/line/src
- extensions/matrix/src
- extensions/mattermost/src
- extensions/msteams/src
- extensions/nextcloud-talk/src
- extensions/nostr/src
- extensions/qa-channel/src
- extensions/qqbot/src
- extensions/signal/src
- extensions/slack/src
- extensions/synology-chat/src
- extensions/telegram/src
- extensions/tlon/src
- extensions/twitch/src
- extensions/whatsapp/src
- extensions/zalo/src
- extensions/zalouser/src
- src/channels
paths-ignore:
- "**/node_modules"
- "**/coverage"
- "**/*.generated.ts"
- "**/*.bundle.js"
- "**/*-runtime.js"
- "**/*.test.ts"
- "**/*.test.tsx"
- "**/*.e2e.test.ts"
- "**/*.e2e.test.tsx"
- "**/*test-support*"
- "**/*test-helper*"
- "**/*mock*"
- "**/*fixture*"
- "**/*bench*"

View File

@@ -1,48 +0,0 @@
name: openclaw-codeql-channel-runtime-boundary-critical-security
disable-default-queries: true
queries:
- uses: security-extended
query-filters:
- include:
precision:
- high
- very-high
tags contain: security
security-severity: /([7-9]|10)\.(\d)+/
paths:
- src/channels
- src/config/channel-*.ts
- src/config/types.channel*.ts
- src/gateway/server-channel*.ts
- src/gateway/server-methods/channels.ts
- src/gateway/protocol/schema/channels.ts
- src/infra/channel-*.ts
- src/infra/exec-approval-channel-runtime.ts
- src/infra/outbound/channel-*.ts
- src/plugin-sdk/channel-*.ts
- src/plugins/channel-*.ts
- src/plugins/bundled-channel-*.ts
- src/plugins/runtime/*channel*.ts
- src/secrets/channel-*.ts
- src/secrets/runtime-config-collectors-channels.ts
- src/security/audit-channel*.ts
paths-ignore:
- "**/node_modules"
- "**/coverage"
- "**/*.generated.ts"
- "**/*.bundle.js"
- "**/*-runtime.js"
- "**/*.test.ts"
- "**/*.test.tsx"
- "**/*.e2e.test.ts"
- "**/*.e2e.test.tsx"
- "**/*test-support*"
- "**/*test-helper*"
- "**/*mock*"
- "**/*fixture*"
- "**/*bench*"

View File

@@ -1,33 +0,0 @@
name: openclaw-codeql-config-boundary-critical-quality
disable-default-queries: true
queries:
- uses: security-and-quality
query-filters:
- include:
problem.severity:
- error
- exclude:
tags:
- security
paths:
- src/config
paths-ignore:
- "**/node_modules"
- "**/coverage"
- "**/*.generated.ts"
- "**/*.bundle.js"
- "**/*-runtime.js"
- "**/*.test.ts"
- "**/*.test.tsx"
- "**/*.e2e.test.ts"
- "**/*.e2e.test.tsx"
- "**/*test-support*"
- "**/*test-helper*"
- "**/*mock*"
- "**/*fixture*"
- "**/*bench*"

View File

@@ -1,53 +0,0 @@
name: openclaw-codeql-core-auth-secrets-critical-quality
disable-default-queries: true
queries:
- uses: security-and-quality
query-filters:
- include:
problem.severity:
- error
- exclude:
tags:
- security
paths:
- src/agents/*auth*.ts
- src/agents/**/*auth*.ts
- src/agents/auth-health*.ts
- src/agents/auth-profiles
- src/agents/bash-tools.exec-host-shared.ts
- src/agents/sandbox
- src/agents/sandbox.ts
- src/agents/sandbox-*.ts
- src/cron/service/jobs.ts
- src/cron/stagger.ts
- src/gateway/*auth*.ts
- src/gateway/**/*auth*.ts
- src/gateway/*secret*.ts
- src/gateway/**/*secret*.ts
- src/gateway/protocol/**/*secret*.ts
- src/gateway/resolve-configured-secret-input-string*.ts
- src/gateway/security-path*.ts
- src/gateway/server-methods/secrets*.ts
- src/infra/secret-file*.ts
- src/secrets
- src/security
paths-ignore:
- "**/node_modules"
- "**/coverage"
- "**/*.generated.ts"
- "**/*.bundle.js"
- "**/*-runtime.js"
- "**/*.test.ts"
- "**/*.test.tsx"
- "**/*.e2e.test.ts"
- "**/*.e2e.test.tsx"
- "**/*test-support*"
- "**/*test-helper*"
- "**/*mock*"
- "**/*fixture*"
- "**/*bench*"

View File

@@ -1,55 +0,0 @@
name: openclaw-codeql-core-auth-secrets-critical-security
disable-default-queries: true
queries:
- uses: security-extended
query-filters:
- include:
precision:
- high
- very-high
tags contain: security
security-severity: /([7-9]|10)\.(\d)+/
paths:
- src/agents/*auth*.ts
- src/agents/**/*auth*.ts
- src/agents/auth-health*.ts
- src/agents/auth-profiles
- src/agents/bash-tools.exec-host-shared.ts
- src/agents/sandbox
- src/agents/sandbox.ts
- src/agents/sandbox-*.ts
- src/config/*secret*.ts
- src/config/**/*secret*.ts
- src/cron/service/jobs.ts
- src/cron/stagger.ts
- src/gateway/*auth*.ts
- src/gateway/**/*auth*.ts
- src/gateway/*secret*.ts
- src/gateway/**/*secret*.ts
- src/gateway/protocol/**/*secret*.ts
- src/gateway/resolve-configured-secret-input-string*.ts
- src/gateway/security-path*.ts
- src/gateway/server-methods/secrets*.ts
- src/infra/secret-file*.ts
- src/secrets
- src/security
paths-ignore:
- "**/node_modules"
- "**/coverage"
- "**/*.generated.ts"
- "**/*.bundle.js"
- "**/*-runtime.js"
- "**/*.test.ts"
- "**/*.test.tsx"
- "**/*.e2e.test.ts"
- "**/*.e2e.test.tsx"
- "**/*test-support*"
- "**/*test-helper*"
- "**/*mock*"
- "**/*fixture*"
- "**/*bench*"

View File

@@ -1,37 +0,0 @@
name: openclaw-codeql-gateway-runtime-boundary-critical-quality
disable-default-queries: true
queries:
- uses: security-and-quality
query-filters:
- include:
problem.severity:
- error
- exclude:
tags:
- security
paths:
- src/gateway/method-scopes.ts
- src/gateway/protocol
- src/gateway/server-methods
- src/gateway/server-methods.ts
- src/gateway/server-methods-list.ts
paths-ignore:
- "**/node_modules"
- "**/coverage"
- "**/*.generated.ts"
- "**/*.bundle.js"
- "**/*-runtime.js"
- "**/*.test.ts"
- "**/*.test.tsx"
- "**/*.e2e.test.ts"
- "**/*.e2e.test.tsx"
- "**/*test-support*"
- "**/*test-helper*"
- "**/*mock*"
- "**/*fixture*"
- "**/*bench*"

View File

@@ -0,0 +1,21 @@
name: openclaw-codeql-javascript-typescript
paths:
- src
- extensions
- ui/src
- skills
paths-ignore:
- apps
- dist
- docs
- "**/node_modules"
- "**/coverage"
- "**/*.generated.ts"
- "**/*.bundle.js"
- "**/*-runtime.js"
- "**/*.test.ts"
- "**/*.test.tsx"
- "**/*.e2e.test.ts"
- "**/*.e2e.test.tsx"

View File

@@ -1,17 +0,0 @@
name: openclaw-codeql-macos-critical-security
disable-default-queries: true
queries:
- uses: security-extended
paths:
- apps/macos/Sources
paths-ignore:
- "**/.build"
- "**/.build/**"
- "**/DerivedData"
- "**/DerivedData/**"
- "**/*.generated.swift"
- "**/*Tests.swift"

View File

@@ -1,35 +0,0 @@
name: openclaw-codeql-mcp-process-runtime-boundary-critical-quality
disable-default-queries: true
queries:
- uses: security-and-quality
query-filters:
- include:
problem.severity:
- error
- exclude:
tags:
- security
paths:
- src/mcp
- src/process
- src/infra/outbound
paths-ignore:
- "**/node_modules"
- "**/coverage"
- "**/*.generated.ts"
- "**/*.bundle.js"
- "**/*-runtime.js"
- "**/*.test.ts"
- "**/*.test.tsx"
- "**/*.e2e.test.ts"
- "**/*.e2e.test.tsx"
- "**/*test-support*"
- "**/*test-helper*"
- "**/*mock*"
- "**/*fixture*"
- "**/*bench*"

View File

@@ -1,56 +0,0 @@
name: openclaw-codeql-mcp-process-tool-boundary-critical-security
disable-default-queries: true
queries:
- uses: security-extended
query-filters:
- include:
precision:
- high
- very-high
tags contain: security
security-severity: /([7-9]|10)\.(\d)+/
paths:
- src/mcp
- src/process
- src/infra/outbound
- src/agents/bash-tools.exec*.ts
- src/agents/bash-tools.process*.ts
- src/agents/exec-*.ts
- src/agents/execution-contract.ts
- src/agents/openclaw-plugin-tools.ts
- src/agents/openclaw-tools.runtime.ts
- src/agents/openclaw-tools.registration.ts
- src/agents/pi-tool-definition-adapter.ts
- src/agents/pi-tools.abort.ts
- src/agents/pi-tools.before-tool-call*.ts
- src/agents/pi-tools.host-edit.ts
- src/agents/pi-tools-parameter-schema.ts
- src/agents/pi-embedded-runner/effective-tool-policy.ts
- src/agents/pi-embedded-runner/tool-name-allowlist.ts
- src/agents/pi-embedded-runner/tool-schema-runtime.ts
- src/agents/tools/gateway-tool.ts
- src/agents/tools/message-tool.ts
- src/agents/tools/sessions-send-tool.ts
- src/agents/tools/sessions-spawn-tool.ts
- src/agents/tools/subagents-tool.ts
- src/agents/tools/tool-runtime.helpers.ts
paths-ignore:
- "**/node_modules"
- "**/coverage"
- "**/*.generated.ts"
- "**/*.bundle.js"
- "**/*-runtime.js"
- "**/*.test.ts"
- "**/*.test.tsx"
- "**/*.e2e.test.ts"
- "**/*.e2e.test.tsx"
- "**/*test-support*"
- "**/*test-helper*"
- "**/*mock*"
- "**/*fixture*"
- "**/*bench*"

View File

@@ -1,41 +0,0 @@
name: openclaw-codeql-memory-runtime-boundary-critical-quality
disable-default-queries: true
queries:
- uses: security-and-quality
query-filters:
- include:
problem.severity:
- error
- exclude:
tags:
- security
paths:
- packages/memory-host-sdk/src
- src/memory
- src/memory-host-sdk
- src/plugin-sdk/memory-*.ts
- src/plugin-sdk/memory-core-host-*.ts
- src/plugins/memory-*.ts
- src/gateway/server-startup-memory.ts
- src/commands/doctor-memory-search.ts
- src/commands/doctor-cron-dreaming-payload-migration.ts
paths-ignore:
- "**/node_modules"
- "**/coverage"
- "**/*.generated.ts"
- "**/*.bundle.js"
- "**/*-runtime.js"
- "**/*.test.ts"
- "**/*.test.tsx"
- "**/*.e2e.test.ts"
- "**/*.e2e.test.tsx"
- "**/*test-support*"
- "**/*test-helper*"
- "**/*mock*"
- "**/*fixture*"
- "**/*bench*"

View File

@@ -1,41 +0,0 @@
name: openclaw-codeql-network-ssrf-boundary-critical-security
disable-default-queries: true
queries:
- uses: security-extended
query-filters:
- include:
precision:
- high
- very-high
tags contain: security
security-severity: /([7-9]|10)\.(\d)+/
paths:
- src/infra/net
- src/shared/net
- src/agents/tools/web-fetch.ts
- src/agents/tools/web-guarded-fetch.ts
- src/agents/tools/web-shared.ts
- src/plugin-sdk/ssrf-policy.ts
- src/web-fetch
- src/web/provider-runtime-shared.ts
- packages/memory-host-sdk/src/host/ssrf-policy.ts
paths-ignore:
- "**/node_modules"
- "**/coverage"
- "**/*.generated.ts"
- "**/*.bundle.js"
- "**/*-runtime.js"
- "**/*.test.ts"
- "**/*.test.tsx"
- "**/*.e2e.test.ts"
- "**/*.e2e.test.tsx"
- "**/*test-support*"
- "**/*test-helper*"
- "**/*mock*"
- "**/*fixture*"
- "**/*bench*"

View File

@@ -1,76 +0,0 @@
name: openclaw-codeql-plugin-boundary-critical-quality
disable-default-queries: true
queries:
- uses: security-and-quality
query-filters:
- include:
problem.severity:
- error
- exclude:
tags:
- security
paths:
- src/plugins/activation-planner.ts
- src/plugins/api-builder.ts
- src/plugins/bundled-compat.ts
- src/plugins/bundled-dir.ts
- src/plugins/bundled-plugin-metadata.ts
- src/plugins/bundled-public-surface-runtime-root.ts
- src/plugins/bundled-runtime-deps.ts
- src/plugins/bundled-runtime-root.ts
- src/plugins/captured-registration.ts
- src/plugins/config-activation-shared.ts
- src/plugins/config-contracts.ts
- src/plugins/config-normalization-shared.ts
- src/plugins/config-policy.ts
- src/plugins/config-schema.ts
- src/plugins/config-state.ts
- src/plugins/discovery.ts
- src/plugins/effective-plugin-ids.ts
- src/plugins/externalized-bundled-plugins.ts
- src/plugins/installed-plugin-index*.ts
- src/plugins/loader*.ts
- src/plugins/manifest*.ts
- src/plugins/module-export.ts
- src/plugins/package-entrypoints.ts
- src/plugins/plugin-registry*.ts
- src/plugins/provider-contract-public-artifacts.ts
- src/plugins/provider-public-artifacts.ts
- src/plugins/public-surface*.ts
- src/plugins/registry.ts
- src/plugins/registry-types.ts
- src/plugins/runtime
- src/plugins/runtime-state.ts
- src/plugins/runtime.ts
- src/plugins/sdk-alias.ts
- src/plugins/source-loader.ts
- src/plugins/types.ts
- src/plugins/validation-diagnostics.ts
- src/plugins/web-provider-public-artifacts*.ts
- src/plugin-sdk/*entry*.ts
- src/plugin-sdk/*facade*.ts
- src/plugin-sdk/api-baseline.ts
- src/plugin-sdk/config-schema.ts
- src/plugin-sdk/config-types.ts
- src/plugin-sdk/core.ts
- src/plugin-sdk/extension-shared.ts
paths-ignore:
- "**/node_modules"
- "**/coverage"
- "**/*.generated.ts"
- "**/*.bundle.js"
- "**/*-runtime.js"
- "**/*.test.ts"
- "**/*.test.tsx"
- "**/*.e2e.test.ts"
- "**/*.e2e.test.tsx"
- "**/*test-support*"
- "**/*test-helper*"
- "**/*mock*"
- "**/*fixture*"
- "**/*bench*"

View File

@@ -1,36 +0,0 @@
name: openclaw-codeql-plugin-sdk-package-contract-critical-quality
disable-default-queries: true
queries:
- uses: security-and-quality
query-filters:
- include:
problem.severity:
- error
- exclude:
tags:
- security
paths:
- packages/plugin-sdk/src
- packages/plugin-package-contract/src
paths-ignore:
- "**/node_modules"
- "**/coverage"
- "**/*.generated.ts"
- "**/*.bundle.js"
- "**/*-runtime.js"
- "**/*.test.ts"
- "**/*.test.tsx"
- "**/*.spec.ts"
- "**/*.spec.tsx"
- "**/*.e2e.test.ts"
- "**/*.e2e.test.tsx"
- "**/*test-support*"
- "**/*test-helper*"
- "**/*mock*"
- "**/*fixture*"
- "**/*bench*"

View File

@@ -1,44 +0,0 @@
name: openclaw-codeql-plugin-sdk-reply-runtime-critical-quality
disable-default-queries: true
queries:
- uses: security-and-quality
query-filters:
- include:
problem.severity:
- error
- exclude:
tags:
- security
paths:
- src/plugin-sdk/inbound-envelope.ts
- src/plugin-sdk/inbound-reply-dispatch.ts
- src/plugin-sdk/reply-*.ts
- src/plugin-sdk/channel-reply-*.ts
- src/plugin-sdk/delivery-queue-runtime.ts
- src/plugin-sdk/outbound-runtime.ts
- src/plugin-sdk/outbound-send-deps.ts
- src/plugin-sdk/model-session-runtime.ts
- src/plugin-sdk/session-*.ts
- src/plugin-sdk/thread-bindings-runtime.ts
- src/plugin-sdk/thread-bindings-session-runtime.ts
- src/plugin-sdk/conversation-binding-runtime.ts
paths-ignore:
- "**/node_modules"
- "**/coverage"
- "**/*.generated.ts"
- "**/*.bundle.js"
- "**/*-runtime.js"
- "**/*.test.ts"
- "**/*.test.tsx"
- "**/*.e2e.test.ts"
- "**/*.e2e.test.tsx"
- "**/*test-support*"
- "**/*test-helper*"
- "**/*mock*"
- "**/*fixture*"
- "**/*bench*"

View File

@@ -1,87 +0,0 @@
name: openclaw-codeql-plugin-trust-boundary-critical-security
disable-default-queries: true
queries:
- uses: security-extended
query-filters:
- include:
precision:
- high
- very-high
tags contain: security
security-severity: /([7-9]|10)\.(\d)+/
paths:
- src/cli/plugin-install-config-policy.ts
- src/cli/plugin-registry-loader.ts
- src/cli/plugins-command-helpers.ts
- src/cli/plugins-install-command.ts
- src/cli/plugins-install-record-commit.ts
- src/plugins/activation-planner.ts
- src/plugins/bundle-manifest.ts
- src/plugins/bundled-compat.ts
- src/plugins/bundled-dir.ts
- src/plugins/bundled-plugin-metadata.ts
- src/plugins/bundled-plugin-scan.ts
- src/plugins/bundled-runtime-deps*.ts
- src/plugins/bundled-runtime-root.ts
- src/plugins/cli-registry-loader.ts
- src/plugins/config-activation-shared.ts
- src/plugins/config-contracts.ts
- src/plugins/config-policy.ts
- src/plugins/config-schema.ts
- src/plugins/dependency-denylist.ts
- src/plugins/discovery.ts
- src/plugins/effective-plugin-ids.ts
- src/plugins/externalized-bundled-plugins.ts
- src/plugins/install.runtime.ts
- src/plugins/install-source-info.ts
- src/plugins/installed-plugin-index*.ts
- src/plugins/loader*.ts
- src/plugins/manifest*.ts
- src/plugins/marketplace.ts
- src/plugins/module-export.ts
- src/plugins/package-entrypoints.ts
- src/plugins/plugin-config-trust.ts
- src/plugins/plugin-origin.types.ts
- src/plugins/plugin-registry*.ts
- src/plugins/public-surface*.ts
- src/plugins/registry*.ts
- src/plugins/runtime
- src/plugins/runtime-state.ts
- src/plugins/runtime.ts
- src/plugins/source-loader.ts
- src/plugins/update.ts
- src/plugins/validation-diagnostics.ts
- src/plugin-sdk/*entry*.ts
- src/plugin-sdk/*facade*.ts
- src/plugin-sdk/api-baseline.ts
- src/plugin-sdk/config-schema.ts
- src/plugin-sdk/config-types.ts
- src/plugin-sdk/core.ts
- src/plugin-sdk/extension-shared.ts
- packages/plugin-package-contract/src
- packages/plugin-sdk/src/plugin-entry.ts
- packages/plugin-sdk/src/plugin-runtime.ts
- packages/plugin-sdk/src/runtime-env.ts
- packages/plugin-sdk/src/security-runtime.ts
paths-ignore:
- "**/node_modules"
- "**/coverage"
- "**/*.generated.ts"
- "**/*.bundle.js"
- "**/*-runtime.js"
- "**/*.test.ts"
- "**/*.test.tsx"
- "**/*.spec.ts"
- "**/*.spec.tsx"
- "**/*.e2e.test.ts"
- "**/*.e2e.test.tsx"
- "**/*test-support*"
- "**/*test-helper*"
- "**/*mock*"
- "**/*fixture*"
- "**/*bench*"

View File

@@ -1,44 +0,0 @@
name: openclaw-codeql-provider-runtime-boundary-critical-quality
disable-default-queries: true
queries:
- uses: security-and-quality
query-filters:
- include:
problem.severity:
- error
- exclude:
tags:
- security
paths:
- src/model-catalog
- src/plugins/provider-*.ts
- src/plugins/providers*.ts
- src/plugins/*provider*.ts
- src/plugins/capability-provider-runtime.ts
- src/plugins/compaction-provider.ts
- src/plugins/memory-embedding-provider*.ts
- src/plugins/memory-embedding-providers*.ts
- src/plugins/migration-provider-runtime.ts
- src/plugins/synthetic-auth.runtime.ts
- src/plugins/web-fetch-providers*.ts
- src/plugins/web-search-providers*.ts
paths-ignore:
- "**/node_modules"
- "**/coverage"
- "**/*.generated.ts"
- "**/*.bundle.js"
- "**/*-runtime.js"
- "**/*.test.ts"
- "**/*.test.tsx"
- "**/*.e2e.test.ts"
- "**/*.e2e.test.tsx"
- "**/*test-support*"
- "**/*test-helper*"
- "**/*mock*"
- "**/*fixture*"
- "**/*bench*"

View File

@@ -1,48 +0,0 @@
name: openclaw-codeql-session-diagnostics-boundary-critical-quality
disable-default-queries: true
queries:
- uses: security-and-quality
query-filters:
- include:
problem.severity:
- error
- exclude:
tags:
- security
paths:
- src/auto-reply/reply/queue
- src/auto-reply/reply/post-compaction-context.ts
- src/auto-reply/reply/startup-context.ts
- src/infra/diagnostic-*.ts
- src/infra/diagnostics-timeline.ts
- src/infra/session-delivery-queue*.ts
- src/infra/outbound/base-session-key.ts
- src/infra/outbound/delivery-queue*.ts
- src/infra/outbound/outbound-session.ts
- src/infra/outbound/session-binding*.ts
- src/infra/outbound/session-context.ts
- src/infra/outbound/targets-session.ts
- src/logging/diagnostic*.ts
- src/commands/doctor-session-*.ts
- src/commands/session-store-targets.ts
- src/commands/sessions*.ts
paths-ignore:
- "**/node_modules"
- "**/coverage"
- "**/*.generated.ts"
- "**/*.bundle.js"
- "**/*-runtime.js"
- "**/*.test.ts"
- "**/*.test.tsx"
- "**/*.e2e.test.ts"
- "**/*.e2e.test.tsx"
- "**/*test-support*"
- "**/*test-helper*"
- "**/*mock*"
- "**/*fixture*"
- "**/*bench*"

View File

@@ -1,36 +0,0 @@
name: openclaw-codeql-ui-control-plane-critical-quality
disable-default-queries: true
queries:
- uses: security-and-quality
query-filters:
- include:
problem.severity:
- error
- exclude:
tags:
- security
paths:
- ui/src/main.ts
- ui/src/local-storage.ts
- ui/src/ui
- src/tasks/task-registry-control*.ts
paths-ignore:
- "**/node_modules"
- "**/coverage"
- "**/*.generated.ts"
- "**/*.bundle.js"
- "**/*-runtime.js"
- "**/*.test.ts"
- "**/*.test.tsx"
- "**/*.e2e.test.ts"
- "**/*.e2e.test.tsx"
- "**/*test-support*"
- "**/*test-helper*"
- "**/*mock*"
- "**/*fixture*"
- "**/*bench*"

View File

@@ -1,39 +0,0 @@
name: openclaw-codeql-web-media-runtime-boundary-critical-quality
disable-default-queries: true
queries:
- uses: security-and-quality
query-filters:
- include:
problem.severity:
- error
- exclude:
tags:
- security
paths:
- src/web-fetch
- src/web-search
- src/web/provider-runtime-shared.ts
- src/media
- src/media-understanding
- src/image-generation
- src/media-generation
paths-ignore:
- "**/node_modules"
- "**/coverage"
- "**/*.generated.ts"
- "**/*.bundle.js"
- "**/*-runtime.js"
- "**/*.test.ts"
- "**/*.test.tsx"
- "**/*.e2e.test.ts"
- "**/*.e2e.test.tsx"
- "**/*test-support*"
- "**/*test-helper*"
- "**/*mock*"
- "**/*fixture*"
- "**/*bench*"

View File

@@ -1,16 +0,0 @@
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
bash \
ca-certificates \
curl \
ffmpeg \
git \
openssh-client \
unzip \
xz-utils \
zstd \
&& rm -rf /var/lib/apt/lists/*

35
.github/labeler.yml vendored
View File

@@ -9,12 +9,6 @@
- "extensions/azure-speech/**"
- "docs/providers/azure-speech.md"
- "docs/tools/tts.md"
"plugin: file-transfer":
- changed-files:
- any-glob-to-any-file:
- "extensions/file-transfer/**"
- "docs/nodes/index.md"
- "docs/plugins/sdk-runtime.md"
"channel: discord":
- changed-files:
- any-glob-to-any-file:
@@ -41,17 +35,6 @@
- any-glob-to-any-file:
- "extensions/google-meet/**"
- "docs/plugins/google-meet.md"
"plugin: migrate-hermes":
- changed-files:
- any-glob-to-any-file:
- "extensions/migrate-hermes/**"
- "docs/cli/migrate.md"
"plugin: migrate-claude":
- changed-files:
- any-glob-to-any-file:
- "extensions/migrate-claude/**"
- "docs/cli/migrate.md"
- "docs/install/migrating-claude.md"
"plugin: bonjour":
- changed-files:
- any-glob-to-any-file:
@@ -118,11 +101,6 @@
- any-glob-to-any-file:
- "extensions/slack/**"
- "docs/channels/slack.md"
"channel: synology-chat":
- changed-files:
- any-glob-to-any-file:
- "extensions/synology-chat/**"
- "docs/channels/synology-chat.md"
"channel: telegram":
- changed-files:
- any-glob-to-any-file:
@@ -244,11 +222,8 @@
"security":
- changed-files:
- any-glob-to-any-file:
- ".github/workflows/opengrep-*.yml"
- ".semgrepignore"
- "docs/cli/security.md"
- "docs/gateway/security.md"
- "security/**"
"extensions: copilot-proxy":
- changed-files:
@@ -314,20 +289,10 @@
- changed-files:
- any-glob-to-any-file:
- "extensions/byteplus/**"
"extensions: cerebras":
- changed-files:
- any-glob-to-any-file:
- "extensions/cerebras/**"
- "docs/providers/cerebras.md"
"extensions: deepseek":
- changed-files:
- any-glob-to-any-file:
- "extensions/deepseek/**"
"extensions: deepinfra":
- changed-files:
- any-glob-to-any-file:
- "extensions/deepinfra/**"
- "docs/providers/deepinfra.md"
"extensions: tencent":
- changed-files:
- any-glob-to-any-file:

View File

@@ -1,224 +0,0 @@
name: Blacksmith Build Artifacts Testbox
on:
workflow_dispatch:
inputs:
testbox_id:
type: string
description: "Testbox session ID"
required: true
pull_request:
paths:
- ".github/workflows/**"
permissions:
contents: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
build-artifacts:
permissions:
contents: read
name: "build-artifacts"
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 35
steps:
- name: Begin Testbox
uses: useblacksmith/begin-testbox@d0e04585c26905fdd92c94a09c159544c7ee1b67
with:
testbox_id: ${{ inputs.testbox_id }}
- name: Checkout
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
workdir="$GITHUB_WORKSPACE"
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
reset_checkout_dir() {
mkdir -p "$workdir"
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
}
checkout_attempt() {
local attempt="$1"
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1
test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1
echo "checkout attempt ${attempt}/5 succeeded"
}
for attempt in 1 2 3 4 5; do
if checkout_attempt "$attempt"; then
exit 0
fi
echo "checkout attempt ${attempt}/5 failed"
sleep $((attempt * 5))
done
echo "checkout failed after 5 attempts" >&2
exit 1
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
- name: Resolve release dist cache seeds
id: dist-cache-seeds
shell: bash
run: |
set -euo pipefail
cache_prefix="${RUNNER_OS}-dist-build-"
declare -A seen=()
resolve_tag_sha() {
local tag="$1"
local direct=""
local peeled=""
while read -r sha ref; do
if [[ "$ref" == "refs/tags/${tag}^{}" ]]; then
peeled="$sha"
elif [[ "$ref" == "refs/tags/${tag}" ]]; then
direct="$sha"
fi
done < <(git ls-remote --tags origin "refs/tags/${tag}" "refs/tags/${tag}^{}")
printf '%s\n' "${peeled:-$direct}"
}
{
echo "restore-keys<<EOF"
for dist_tag in beta latest; do
version="$(npm view "openclaw@${dist_tag}" version 2>/dev/null || true)"
if [[ -z "$version" ]]; then
echo "Could not resolve npm dist-tag ${dist_tag}; skipping cache seed." >&2
continue
fi
sha="$(resolve_tag_sha "v${version}")"
if [[ -z "$sha" ]]; then
echo "Could not resolve git tag v${version}; skipping cache seed." >&2
continue
fi
key="${cache_prefix}${sha}"
if [[ -z "${seen[$key]+x}" ]]; then
echo "$key"
seen[$key]=1
fi
done
echo "${cache_prefix}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Restore dist build cache
id: dist-cache
uses: actions/cache/restore@v5
with:
path: |
.artifacts/build-all-cache/
dist/
dist-runtime/
key: ${{ runner.os }}-dist-build-${{ github.sha }}
restore-keys: ${{ steps.dist-cache-seeds.outputs.restore-keys }}
- name: Build dist on cache miss
if: steps.dist-cache.outputs.cache-hit != 'true'
run: pnpm build:ci-artifacts
- name: Build Control UI on cache miss
if: steps.dist-cache.outputs.cache-hit != 'true'
run: pnpm ui:build
- name: Verify build artifacts
shell: bash
run: |
set -euo pipefail
test -d dist
test -d dist-runtime
if [[ ! -f dist/index.js && ! -f dist/index.mjs ]]; then
echo "Missing dist/index.js or dist/index.mjs" >&2
exit 1
fi
test -f dist/build-info.json
test -f dist/control-ui/index.html
- name: Save dist build cache
if: steps.dist-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
with:
path: |
.artifacts/build-all-cache/
dist/
dist-runtime/
key: ${{ runner.os }}-dist-build-${{ github.sha }}
- name: Prepare Testbox shell
shell: bash
run: |
set -euo pipefail
git fetch --no-tags --depth=50 origin "+refs/heads/main:refs/remotes/origin/main"
node_bin="$(dirname "$(node -p 'process.execPath')")"
pnpm_bin="$(command -v pnpm)"
sudo ln -sf "$node_bin/node" /usr/local/bin/node
sudo ln -sf "$node_bin/npm" /usr/local/bin/npm
sudo ln -sf "$node_bin/npx" /usr/local/bin/npx
sudo ln -sf "$node_bin/corepack" /usr/local/bin/corepack
sudo ln -sf "$pnpm_bin" /usr/local/bin/pnpm
- name: Hydrate Testbox provider env helper
shell: bash
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
run: bash scripts/ci-hydrate-testbox-env.sh
- name: Run Testbox
uses: useblacksmith/run-testbox@5ca05834db1d3813554d1dd109e5f2087a8d7cbc
if: always()
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

View File

@@ -25,7 +25,7 @@ jobs:
timeout-minutes: 30
steps:
- name: Begin Testbox
uses: useblacksmith/begin-testbox@d0e04585c26905fdd92c94a09c159544c7ee1b67
uses: useblacksmith/begin-testbox@v2
with:
testbox_id: ${{ inputs.testbox_id }}
- name: Checkout
@@ -93,35 +93,8 @@ jobs:
sudo ln -sf "$node_bin/npx" /usr/local/bin/npx
sudo ln -sf "$node_bin/corepack" /usr/local/bin/corepack
sudo ln -sf "$pnpm_bin" /usr/local/bin/pnpm
- name: Hydrate Testbox provider env helper
shell: bash
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
run: bash scripts/ci-hydrate-testbox-env.sh
- name: Run Testbox
uses: useblacksmith/run-testbox@5ca05834db1d3813554d1dd109e5f2087a8d7cbc
uses: useblacksmith/run-testbox@v2
if: always()
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

View File

@@ -2,17 +2,6 @@ name: CI
on:
workflow_dispatch:
inputs:
target_ref:
description: Optional branch, tag, or full commit SHA to validate instead of the workflow ref
required: false
default: ""
type: string
include_android:
description: Run Android lanes for this manual CI dispatch.
required: false
default: false
type: boolean
push:
branches: [main]
paths-ignore:
@@ -41,7 +30,6 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 20
outputs:
checkout_revision: ${{ steps.checkout_ref.outputs.sha }}
docs_only: ${{ steps.manifest.outputs.docs_only }}
docs_changed: ${{ steps.manifest.outputs.docs_changed }}
run_node: ${{ steps.manifest.outputs.run_node }}
@@ -50,19 +38,22 @@ jobs:
run_skills_python: ${{ steps.manifest.outputs.run_skills_python }}
run_skills_python_job: ${{ steps.manifest.outputs.run_skills_python_job }}
run_windows: ${{ steps.manifest.outputs.run_windows }}
has_changed_extensions: ${{ steps.manifest.outputs.has_changed_extensions }}
changed_extensions_matrix: ${{ steps.manifest.outputs.changed_extensions_matrix }}
run_build_artifacts: ${{ steps.manifest.outputs.run_build_artifacts }}
run_checks_fast_core: ${{ steps.manifest.outputs.run_checks_fast_core }}
run_checks_fast: ${{ steps.manifest.outputs.run_checks_fast }}
checks_fast_core_matrix: ${{ steps.manifest.outputs.checks_fast_core_matrix }}
run_plugin_contracts_shards: ${{ steps.manifest.outputs.run_plugin_contracts_shards }}
plugin_contracts_matrix: ${{ steps.manifest.outputs.plugin_contracts_matrix }}
channel_contracts_matrix: ${{ steps.manifest.outputs.channel_contracts_matrix }}
checks_node_extensions_matrix: ${{ steps.manifest.outputs.checks_node_extensions_matrix }}
run_checks: ${{ steps.manifest.outputs.run_checks }}
checks_matrix: ${{ steps.manifest.outputs.checks_matrix }}
run_checks_node_core_nondist: ${{ steps.manifest.outputs.run_checks_node_core_nondist }}
checks_node_core_nondist_matrix: ${{ steps.manifest.outputs.checks_node_core_nondist_matrix }}
run_checks_node_core_dist: ${{ steps.manifest.outputs.run_checks_node_core_dist }}
checks_node_core_dist_matrix: ${{ steps.manifest.outputs.checks_node_core_dist_matrix }}
run_extension_fast: ${{ steps.manifest.outputs.run_extension_fast }}
extension_fast_matrix: ${{ steps.manifest.outputs.extension_fast_matrix }}
run_check: ${{ steps.manifest.outputs.run_check }}
run_check_additional: ${{ steps.manifest.outputs.run_check_additional }}
run_build_smoke: ${{ steps.manifest.outputs.run_build_smoke }}
@@ -79,16 +70,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ inputs.target_ref || github.sha }}
fetch-depth: 1
fetch-tags: false
persist-credentials: false
submodules: false
- name: Resolve checkout SHA
id: checkout_ref
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Ensure preflight base commit
if: github.event_name != 'workflow_dispatch'
uses: ./.github/actions/ensure-base-commit
@@ -116,6 +102,29 @@ jobs:
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD
- name: Detect changed extensions
id: changed_extensions
if: github.event_name != 'workflow_dispatch' && steps.docs_scope.outputs.docs_only != 'true' && steps.changed_scope.outputs.run_node == 'true'
env:
BASE_SHA: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
BASE_REF: ${{ github.event_name == 'push' && github.ref_name || github.event.pull_request.base.ref }}
run: |
node --input-type=module <<'EOF'
import { appendFileSync } from "node:fs";
import { listChangedExtensionIds } from "./scripts/lib/changed-extensions.mjs";
const extensionIds = listChangedExtensionIds({
base: process.env.BASE_SHA,
head: "HEAD",
fallbackBaseRef: process.env.BASE_REF,
unavailableBaseBehavior: "all",
});
const matrix = JSON.stringify({ include: extensionIds.map((extension) => ({ extension })) });
appendFileSync(process.env.GITHUB_OUTPUT, `has_changed_extensions=${extensionIds.length > 0}\n`, "utf8");
appendFileSync(process.env.GITHUB_OUTPUT, `changed_extensions_matrix=${matrix}\n`, "utf8");
EOF
- name: Build CI manifest
id: manifest
env:
@@ -123,14 +132,15 @@ jobs:
OPENCLAW_CI_DOCS_CHANGED: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.docs_scope.outputs.docs_changed }}
OPENCLAW_CI_RUN_NODE: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_node || 'false' }}
OPENCLAW_CI_RUN_MACOS: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_macos || 'false' }}
OPENCLAW_CI_RUN_ANDROID: ${{ github.event_name == 'workflow_dispatch' && inputs.include_android && 'true' || steps.changed_scope.outputs.run_android || 'false' }}
OPENCLAW_CI_RUN_ANDROID: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_android || 'false' }}
OPENCLAW_CI_RUN_WINDOWS: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_windows || 'false' }}
OPENCLAW_CI_RUN_NODE_FAST_ONLY: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.changed_scope.outputs.run_node_fast_only || 'false' }}
OPENCLAW_CI_RUN_NODE_FAST_PLUGIN_CONTRACTS: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.changed_scope.outputs.run_node_fast_plugin_contracts || 'false' }}
OPENCLAW_CI_RUN_NODE_FAST_CI_ROUTING: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.changed_scope.outputs.run_node_fast_ci_routing || 'false' }}
OPENCLAW_CI_RUN_SKILLS_PYTHON: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_skills_python || 'false' }}
OPENCLAW_CI_RUN_CONTROL_UI_I18N: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_control_ui_i18n || 'false' }}
OPENCLAW_CI_CHECKOUT_REVISION: ${{ steps.checkout_ref.outputs.sha }}
OPENCLAW_CI_HAS_CHANGED_EXTENSIONS: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.changed_extensions.outputs.has_changed_extensions || 'false' }}
OPENCLAW_CI_CHANGED_EXTENSIONS_MATRIX: ${{ github.event_name == 'workflow_dispatch' && '{"include":[]}' || steps.changed_extensions.outputs.changed_extensions_matrix || '{"include":[]}' }}
OPENCLAW_CI_REPOSITORY: ${{ github.repository }}
run: |
node --input-type=module <<'EOF'
@@ -141,6 +151,10 @@ jobs:
import {
createChannelContractTestShards,
} from "./scripts/lib/channel-contract-test-plan.mjs";
import {
createExtensionTestShards,
DEFAULT_EXTENSION_TEST_SHARD_COUNT,
} from "./scripts/lib/extension-test-plan.mjs";
const parseBoolean = (value, fallback = false) => {
if (value === undefined) return fallback;
@@ -150,26 +164,18 @@ jobs:
return fallback;
};
const { createPluginContractTestShards } = await import(
"./scripts/lib/plugin-contract-test-plan.mjs"
).catch((error) => {
if (error?.code !== "ERR_MODULE_NOT_FOUND") {
throw error;
const parseJson = (value, fallback) => {
try {
return value ? JSON.parse(value) : fallback;
} catch {
return fallback;
}
return {
createPluginContractTestShards: () => [
{
checkName: "checks-fast-contracts-plugins-legacy",
includePatterns: ["src/plugins/contracts/**/*.test.ts"],
runtime: "node",
task: "contracts-plugins",
},
],
};
});
};
const createMatrix = (include) => ({ include });
const outputPath = process.env.GITHUB_OUTPUT;
const eventName = process.env.GITHUB_EVENT_NAME ?? "pull_request";
const isPush = eventName === "push";
const isCanonicalRepository = process.env.OPENCLAW_CI_REPOSITORY === "openclaw/openclaw";
const docsOnly = parseBoolean(process.env.OPENCLAW_CI_DOCS_ONLY);
const docsChanged = parseBoolean(process.env.OPENCLAW_CI_DOCS_CHANGED);
@@ -181,7 +187,7 @@ jobs:
runNode && parseBoolean(process.env.OPENCLAW_CI_RUN_NODE_FAST_PLUGIN_CONTRACTS);
const runNodeFastCiRouting =
runNode && parseBoolean(process.env.OPENCLAW_CI_RUN_NODE_FAST_CI_ROUTING);
const runPluginContractShards = runNodeFull || runNodeFastPluginContracts;
const runChecksFastCore = runNodeFull || runNodeFastPluginContracts || runNodeFastCiRouting;
const runMacos =
parseBoolean(process.env.OPENCLAW_CI_RUN_MACOS) && !docsOnly && isCanonicalRepository;
const runAndroid =
@@ -194,13 +200,49 @@ jobs:
const runSkillsPython = parseBoolean(process.env.OPENCLAW_CI_RUN_SKILLS_PYTHON) && !docsOnly;
const runControlUiI18n =
parseBoolean(process.env.OPENCLAW_CI_RUN_CONTROL_UI_I18N) && !docsOnly;
const hasChangedExtensions =
parseBoolean(process.env.OPENCLAW_CI_HAS_CHANGED_EXTENSIONS) && !docsOnly;
const changedExtensionsMatrix = hasChangedExtensions
? parseJson(process.env.OPENCLAW_CI_CHANGED_EXTENSIONS_MATRIX, { include: [] })
: { include: [] };
const extensionTestShardCount = isCanonicalRepository
? DEFAULT_EXTENSION_TEST_SHARD_COUNT
: Math.max(DEFAULT_EXTENSION_TEST_SHARD_COUNT, 36);
const extensionShardMatrix = createMatrix(
runNodeFull
? createExtensionTestShards({
shardCount: extensionTestShardCount,
}).map((shard) => ({
check_name: shard.checkName,
extensions_csv: shard.extensionIds.join(","),
runner: isCanonicalRepository && [0, 3, 4].includes(shard.index)
? "blacksmith-8vcpu-ubuntu-2404"
: isCanonicalRepository
? "blacksmith-4vcpu-ubuntu-2404"
: "ubuntu-24.04",
shard_index: shard.index + 1,
task: "extensions-batch",
}))
: [],
);
const checksFastCoreTasks = [];
if (runNodeFull) {
checksFastCoreTasks.push(
{ check_name: "checks-fast-bundled", runtime: "node", task: "bundled" },
{
check_name: "checks-fast-contracts-plugins",
runtime: "node",
task: "contracts-plugins",
},
);
} else {
if (runNodeFastCiRouting) {
if (runNodeFastPluginContracts) {
checksFastCoreTasks.push({
check_name: "checks-fast-contracts-plugins",
runtime: "node",
task: runNodeFastCiRouting ? "contracts-plugins-ci-routing" : "contracts-plugins",
});
} else if (runNodeFastCiRouting) {
checksFastCoreTasks.push({
check_name: "checks-fast-ci-routing",
runtime: "node",
@@ -210,9 +252,7 @@ jobs:
}
const nodeTestShards = runNodeFull
? createNodeTestShards({
includeReleaseOnlyPluginShards: false,
}).map((shard) => ({
? createNodeTestShards().map((shard) => ({
check_name: shard.checkName,
runtime: "node",
task: "test-shard",
@@ -234,17 +274,16 @@ jobs:
run_android: runAndroid,
run_skills_python: runSkillsPython,
run_windows: runWindows,
has_changed_extensions: hasChangedExtensions,
changed_extensions_matrix: changedExtensionsMatrix,
run_build_artifacts: runNodeFull,
run_checks_fast_core: checksFastCoreTasks.length > 0,
run_checks_fast_core: runChecksFastCore,
run_checks_fast: runNodeFull,
checks_fast_core_matrix: createMatrix(checksFastCoreTasks),
run_plugin_contracts_shards: runPluginContractShards,
plugin_contracts_matrix: createMatrix(
runPluginContractShards ? createPluginContractTestShards() : [],
),
channel_contracts_matrix: createMatrix(
runNodeFull ? createChannelContractTestShards() : [],
),
checks_node_extensions_matrix: extensionShardMatrix,
run_checks: runNodeFull,
checks_matrix: createMatrix(
runNodeFull
@@ -257,6 +296,15 @@ jobs:
checks_node_core_nondist_matrix: createMatrix(nodeTestNonDistShards),
run_checks_node_core_dist: nodeTestDistShards.length > 0,
checks_node_core_dist_matrix: createMatrix(nodeTestDistShards),
run_extension_fast: hasChangedExtensions && !isPush,
extension_fast_matrix: createMatrix(
hasChangedExtensions && !isPush
? (changedExtensionsMatrix.include ?? []).map((entry) => ({
check_name: `extension-fast-${entry.extension}`,
extension: entry.extension,
}))
: [],
),
run_check: runNodeFull,
run_check_additional: runNodeFull,
run_build_smoke: runNodeFull,
@@ -309,14 +357,12 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ inputs.target_ref || github.sha }}
fetch-depth: 1
fetch-tags: false
persist-credentials: false
submodules: false
- name: Ensure security base commit
if: github.event_name != 'workflow_dispatch'
uses: ./.github/actions/ensure-base-commit
with:
base-sha: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
@@ -400,7 +446,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ inputs.target_ref || github.sha }}
fetch-depth: 1
fetch-tags: false
persist-credentials: false
@@ -463,7 +508,7 @@ jobs:
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -535,7 +580,7 @@ jobs:
path: |
dist/
dist-runtime/
key: ${{ runner.os }}-dist-build-${{ needs.preflight.outputs.checkout_revision }}
key: ${{ runner.os }}-dist-build-${{ github.sha }}
- name: Pack built runtime artifacts
run: tar --posix -cf dist-runtime-build.tar.zst --use-compress-program zstdmt dist dist-runtime
@@ -664,7 +709,7 @@ jobs:
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -743,112 +788,6 @@ jobs:
;;
esac
checks-fast-plugin-contracts-shard:
permissions:
contents: read
name: ${{ matrix.checkName }}
needs: [preflight]
if: needs.preflight.outputs.run_plugin_contracts_shards == 'true'
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.preflight.outputs.plugin_contracts_matrix) }}
steps:
- name: Checkout
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
workdir="$GITHUB_WORKSPACE"
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
reset_checkout_dir() {
mkdir -p "$workdir"
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
}
checkout_attempt() {
local attempt="$1"
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1
test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1
echo "checkout attempt ${attempt}/5 succeeded"
}
for attempt in 1 2 3 4 5; do
if checkout_attempt "$attempt"; then
exit 0
fi
echo "checkout attempt ${attempt}/5 failed"
sleep $((attempt * 5))
done
echo "checkout failed after 5 attempts" >&2
exit 1
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
- name: Run plugin contract shard
env:
OPENCLAW_CONTRACT_INCLUDE_PATTERNS_JSON: ${{ toJson(matrix.includePatterns) }}
shell: bash
run: |
set -euo pipefail
include_file="$RUNNER_TEMP/plugin-contract-include.json"
INCLUDE_FILE="$include_file" node --input-type=module <<'EOF'
import { writeFileSync } from "node:fs";
const includePatterns = JSON.parse(process.env.OPENCLAW_CONTRACT_INCLUDE_PATTERNS_JSON ?? "[]");
if (!Array.isArray(includePatterns) || includePatterns.length === 0) {
console.error("Missing plugin contract include patterns");
process.exit(1);
}
writeFileSync(process.env.INCLUDE_FILE, JSON.stringify(includePatterns), "utf8");
EOF
OPENCLAW_VITEST_INCLUDE_FILE="$include_file" pnpm test:contracts:plugins
checks-fast-plugin-contracts:
permissions:
contents: read
name: checks-fast-contracts-plugins
needs: [preflight, checks-fast-plugin-contracts-shard]
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_plugin_contracts_shards == 'true' }}
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Verify plugin contract shards
env:
SHARD_RESULT: ${{ needs.checks-fast-plugin-contracts-shard.result }}
run: |
if [ "$SHARD_RESULT" = "cancelled" ]; then
echo "Plugin contract shards were cancelled, usually because a newer commit superseded this run." >&2
exit 1
fi
if [ "$SHARD_RESULT" != "success" ]; then
echo "Plugin contract shards failed: $SHARD_RESULT" >&2
exit 1
fi
checks-fast-channel-contracts-shard:
permissions:
contents: read
@@ -865,7 +804,7 @@ jobs:
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -968,7 +907,7 @@ jobs:
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -1020,6 +959,97 @@ jobs:
- name: Run protocol check
run: pnpm protocol:check
checks-node-extensions-shard:
permissions:
contents: read
name: ${{ matrix.check_name }}
needs: [preflight]
if: needs.preflight.outputs.run_checks_fast == 'true'
runs-on: ${{ matrix.runner }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.preflight.outputs.checks_node_extensions_matrix) }}
steps:
- name: Checkout
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
workdir="$GITHUB_WORKSPACE"
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
reset_checkout_dir() {
mkdir -p "$workdir"
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
}
checkout_attempt() {
local attempt="$1"
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1
test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1
echo "checkout attempt ${attempt}/5 succeeded"
}
for attempt in 1 2 3 4 5; do
if checkout_attempt "$attempt"; then
exit 0
fi
echo "checkout attempt ${attempt}/5 failed"
sleep $((attempt * 5))
done
echo "checkout failed after 5 attempts" >&2
exit 1
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
- name: Run extension shard
env:
NODE_OPTIONS: --max-old-space-size=6144
OPENCLAW_EXTENSION_BATCH_PARALLEL: 2
OPENCLAW_VITEST_MAX_WORKERS: 1
OPENCLAW_EXTENSION_BATCH: ${{ matrix.extensions_csv }}
run: pnpm test:extensions:batch -- "$OPENCLAW_EXTENSION_BATCH"
checks-node-extensions:
permissions:
contents: read
name: checks-node-extensions
needs: [preflight, checks-node-extensions-shard]
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_checks_fast == 'true' }}
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Verify extension shards
env:
SHARD_RESULT: ${{ needs.checks-node-extensions-shard.result }}
run: |
if [ "$SHARD_RESULT" != "success" ]; then
echo "Extension shard checks failed: $SHARD_RESULT" >&2
exit 1
fi
checks:
permissions:
contents: read
@@ -1057,7 +1087,7 @@ jobs:
contents: read
name: checks-node-compat-node22
needs: [preflight]
if: needs.preflight.outputs.run_build_artifacts == 'true' && github.event_name == 'workflow_dispatch'
if: needs.preflight.outputs.run_build_artifacts == 'true' && github.event_name == 'push'
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 60
steps:
@@ -1065,7 +1095,7 @@ jobs:
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -1145,7 +1175,7 @@ jobs:
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -1296,6 +1326,84 @@ jobs:
exit 1
fi
extension-fast:
permissions:
contents: read
name: "extension-fast"
needs: [preflight]
if: needs.preflight.outputs.run_extension_fast == 'true'
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.preflight.outputs.extension_fast_matrix) }}
steps:
- name: Checkout
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
workdir="$GITHUB_WORKSPACE"
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
reset_checkout_dir() {
mkdir -p "$workdir"
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
}
checkout_attempt() {
local attempt="$1"
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1
test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1
echo "checkout attempt ${attempt}/5 succeeded"
}
for attempt in 1 2 3 4 5; do
if checkout_attempt "$attempt"; then
exit 0
fi
echo "checkout attempt ${attempt}/5 failed"
sleep $((attempt * 5))
done
echo "checkout failed after 5 attempts" >&2
exit 1
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
- name: Run changed extension tests
env:
OPENCLAW_CHANGED_EXTENSION: ${{ matrix.extension }}
run: |
set -euo pipefail
if [ "$OPENCLAW_CHANGED_EXTENSION" = "telegram" ]; then
export OPENCLAW_VITEST_MAX_WORKERS=1
export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--max-old-space-size=6144"
pnpm test:extension "$OPENCLAW_CHANGED_EXTENSION" -- --pool=forks
exit 0
fi
pnpm test:extension "$OPENCLAW_CHANGED_EXTENSION"
# Types, lint, and format check shards.
check-shard:
permissions:
@@ -1318,9 +1426,6 @@ jobs:
- check_name: check-lint
task: lint
runner: blacksmith-16vcpu-ubuntu-2404
- check_name: check-dependencies
task: dependencies
runner: ubuntu-24.04
- check_name: check-policy-guards
task: policy-guards
runner: ubuntu-24.04
@@ -1335,7 +1440,7 @@ jobs:
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -1396,7 +1501,6 @@ jobs:
pnpm check:no-conflict-markers
pnpm tool-display:check
pnpm check:host-env-policy:swift
pnpm dup:check:coverage
;;
prod-types)
pnpm tsgo:prod
@@ -1404,14 +1508,6 @@ jobs:
lint)
pnpm lint --threads=8
;;
dependencies)
if pnpm run --silent 2>/dev/null | grep -q '^ deadcode:dependencies$'; then
pnpm deadcode:dependencies
pnpm deadcode:unused-files
else
pnpm deadcode:ci
fi
;;
policy-guards)
pnpm lint:webhook:no-low-level-body-read
pnpm lint:auth:no-pairing-store-group
@@ -1476,7 +1572,7 @@ jobs:
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -1674,7 +1770,7 @@ jobs:
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -1737,7 +1833,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.checkout_revision }}
persist-credentials: false
submodules: false
@@ -1780,7 +1875,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.checkout_revision }}
persist-credentials: false
submodules: false
@@ -1885,7 +1979,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.checkout_revision }}
persist-credentials: false
submodules: false
@@ -1926,7 +2019,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.checkout_revision }}
persist-credentials: false
submodules: false
@@ -2027,7 +2119,7 @@ jobs:
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -2083,14 +2175,6 @@ jobs:
apps/android/**/gradle-wrapper.properties
apps/android/gradle/libs.versions.toml
- name: Cache Android SDK
uses: actions/cache@v5
with:
path: ~/.android-sdk
key: ${{ runner.os }}-android-sdk-v1-cmdline-12266719-platform-36-build-tools-36.0.0
restore-keys: |
${{ runner.os }}-android-sdk-v1-
- name: Setup Android SDK cmdline-tools
run: |
set -euo pipefail
@@ -2099,13 +2183,11 @@ jobs:
ARCHIVE="commandlinetools-linux-${CMDLINE_TOOLS_VERSION}_latest.zip"
URL="https://dl.google.com/android/repository/${ARCHIVE}"
if [ ! -x "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" ]; then
mkdir -p "$ANDROID_SDK_ROOT/cmdline-tools"
curl -fsSL "$URL" -o "/tmp/${ARCHIVE}"
rm -rf "$ANDROID_SDK_ROOT/cmdline-tools/latest"
unzip -q "/tmp/${ARCHIVE}" -d "$ANDROID_SDK_ROOT/cmdline-tools"
mv "$ANDROID_SDK_ROOT/cmdline-tools/cmdline-tools" "$ANDROID_SDK_ROOT/cmdline-tools/latest"
fi
mkdir -p "$ANDROID_SDK_ROOT/cmdline-tools"
curl -fsSL "$URL" -o "/tmp/${ARCHIVE}"
rm -rf "$ANDROID_SDK_ROOT/cmdline-tools/latest"
unzip -q "/tmp/${ARCHIVE}" -d "$ANDROID_SDK_ROOT/cmdline-tools"
mv "$ANDROID_SDK_ROOT/cmdline-tools/cmdline-tools" "$ANDROID_SDK_ROOT/cmdline-tools/latest"
echo "ANDROID_SDK_ROOT=$ANDROID_SDK_ROOT" >> "$GITHUB_ENV"
echo "ANDROID_HOME=$ANDROID_SDK_ROOT" >> "$GITHUB_ENV"

View File

@@ -1,171 +0,0 @@
name: ClawSweeper Dispatch
on:
issues:
types: [opened, reopened, edited, labeled, unlabeled]
issue_comment:
types: [created, edited]
push:
branches: [main]
pull_request_target: # zizmor: ignore[dangerous-triggers] maintainer-owned external dispatch; no checkout or untrusted PR code execution
types: [opened, reopened, synchronize, ready_for_review, edited, labeled, unlabeled]
permissions:
contents: read
concurrency:
group: clawsweeper-dispatch-${{ github.repository }}-${{ github.event.issue.number || github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event.action == 'edited' || github.event.action == 'synchronize' || github.event.action == 'ready_for_review' }}
jobs:
dispatch:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'issue_comment' || !(endsWith(github.actor, '[bot]') && (github.event.action == 'labeled' || github.event.action == 'unlabeled')) }}
env:
HAS_CLAWSWEEPER_APP_PRIVATE_KEY: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY != '' }}
CLAWSWEEPER_APP_CLIENT_ID: Iv23liOECG0slfuhz093
SUPERSEDES_IN_PROGRESS: ${{ (github.event.action == 'edited' || github.event.action == 'synchronize' || github.event.action == 'ready_for_review') && 'true' || 'false' }}
steps:
- name: Debounce bursty metadata events
if: ${{ github.event.action == 'labeled' || github.event.action == 'unlabeled' }}
run: sleep 20
- name: Create ClawSweeper dispatch token
id: token
if: ${{ env.HAS_CLAWSWEEPER_APP_PRIVATE_KEY == 'true' }}
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }}
private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }}
owner: openclaw
repositories: clawsweeper
permission-contents: write
- name: Create target comment token
id: target_token
if: ${{ github.event_name == 'issue_comment' && env.HAS_CLAWSWEEPER_APP_PRIVATE_KEY == 'true' }}
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }}
private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: ${{ github.event.repository.name }}
permission-issues: write
permission-pull-requests: read
- name: Dispatch exact ClawSweeper review
if: ${{ github.event_name != 'push' && github.event_name != 'issue_comment' }}
env:
GH_TOKEN: ${{ steps.token.outputs.token }}
TARGET_REPO: ${{ github.repository }}
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 }}
SOURCE_ACTION: ${{ github.event.action }}
run: |
if [ -z "$GH_TOKEN" ]; then
echo "::notice::Skipping ClawSweeper dispatch because no ClawSweeper app token is configured. Not falling back to a maintainer token."
exit 0
fi
payload="$(jq -nc \
--arg target_repo "$TARGET_REPO" \
--argjson item_number "$ITEM_NUMBER" \
--arg item_kind "$ITEM_KIND" \
--arg source_event "$SOURCE_EVENT" \
--arg source_action "$SOURCE_ACTION" \
--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}}')"
if gh api repos/openclaw/clawsweeper/dispatches \
--method POST \
--input - <<< "$payload"; then
echo "Dispatched ClawSweeper review."
else
echo "::warning::Skipping ClawSweeper dispatch because the configured credential could not dispatch to openclaw/clawsweeper."
fi
- name: Acknowledge and dispatch ClawSweeper comment
if: ${{ github.event_name == 'issue_comment' }}
env:
DISPATCH_TOKEN: ${{ steps.token.outputs.token }}
TARGET_TOKEN: ${{ steps.target_token.outputs.token }}
TARGET_REPO: ${{ github.repository }}
ITEM_NUMBER: ${{ github.event.issue.number }}
COMMENT_ID: ${{ github.event.comment.id }}
COMMENT_BODY: ${{ github.event.comment.body }}
SOURCE_ACTION: ${{ github.event.action }}
run: |
set -euo pipefail
if [ -z "$DISPATCH_TOKEN" ]; then
echo "::notice::Skipping ClawSweeper comment dispatch because no ClawSweeper app token is configured."
exit 0
fi
body_file="$RUNNER_TEMP/clawsweeper-comment-body.txt"
printf '%s\n' "$COMMENT_BODY" > "$body_file"
if ! grep -Eiq '(^|[[:space:]])@(clawsweeper|openclaw-clawsweeper)\b(\[bot\])?|(^|[[:space:]])/(clawsweeper|review|automerge|autoclose)\b' "$body_file"; then
echo "No ClawSweeper command found in comment."
exit 0
fi
if [ -n "$TARGET_TOKEN" ]; then
err="$(mktemp)"
if GH_TOKEN="$TARGET_TOKEN" gh api -X POST \
-H "Accept: application/vnd.github+json" \
"repos/$TARGET_REPO/issues/comments/$COMMENT_ID/reactions" \
-f content="eyes" 2>"$err" >/dev/null; then
echo "Acknowledged ClawSweeper command comment."
elif grep -qi "HTTP 422\\|already exists" "$err"; then
echo "ClawSweeper command comment already acknowledged."
else
cat "$err" >&2
echo "::warning::Could not acknowledge ClawSweeper command comment."
fi
rm -f "$err"
else
echo "::notice::Skipping ClawSweeper comment acknowledgement because no target token is configured."
fi
payload="$(jq -nc \
--arg target_repo "$TARGET_REPO" \
--argjson item_number "$ITEM_NUMBER" \
--argjson comment_id "$COMMENT_ID" \
--arg source_event "issue_comment" \
--arg source_action "$SOURCE_ACTION" \
'{event_type:"clawsweeper_comment",client_payload:{target_repo:$target_repo,item_number:$item_number,comment_id:$comment_id,source_event:$source_event,source_action:$source_action}}')"
if GH_TOKEN="$DISPATCH_TOKEN" gh api repos/openclaw/clawsweeper/dispatches \
--method POST \
--input - <<< "$payload"; then
echo "Dispatched ClawSweeper comment router."
else
echo "::warning::Skipping ClawSweeper comment dispatch because the configured credential could not dispatch to openclaw/clawsweeper."
fi
- name: Dispatch ClawSweeper commit review
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && github.event.deleted != true }}
env:
GH_TOKEN: ${{ steps.token.outputs.token }}
TARGET_REPO: ${{ github.repository }}
BEFORE_SHA: ${{ github.event.before }}
AFTER_SHA: ${{ github.sha }}
SOURCE_REF: ${{ github.ref }}
CREATE_CHECKS: ${{ vars.CLAWSWEEPER_COMMIT_REVIEW_CREATE_CHECKS || 'false' }}
run: |
if [ -z "$GH_TOKEN" ]; then
echo "::notice::Skipping ClawSweeper commit dispatch because no ClawSweeper app token is configured. Not falling back to a maintainer token."
exit 0
fi
case "$CREATE_CHECKS" in
true|TRUE|1|yes|YES|on|ON) create_checks=true ;;
*) create_checks=false ;;
esac
payload="$(jq -nc \
--arg target_repo "$TARGET_REPO" \
--arg before_sha "$BEFORE_SHA" \
--arg after_sha "$AFTER_SHA" \
--arg ref "$SOURCE_REF" \
--argjson create_checks "$create_checks" \
'{event_type:"clawsweeper_commit_review",client_payload:{target_repo:$target_repo,before_sha:$before_sha,after_sha:$after_sha,ref:$ref,enabled:true,create_checks:$create_checks}}')"
if gh api repos/openclaw/clawsweeper/dispatches \
--method POST \
--input - <<< "$payload"; then
echo "Dispatched ClawSweeper commit review."
else
echo "::warning::Skipping ClawSweeper commit dispatch because the configured credential could not dispatch to openclaw/clawsweeper."
fi

View File

@@ -1,51 +0,0 @@
name: CodeQL Android Critical Security
on:
workflow_dispatch:
schedule:
- cron: "0 7 * * *"
concurrency:
group: codeql-android-critical-security-${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.sha }}
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
permissions:
actions: read
contents: read
security-events: write
jobs:
android:
name: Critical Security (android)
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
submodules: false
- name: Setup Java
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
distribution: temurin
java-version: "21"
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: java-kotlin
build-mode: manual
config-file: ./.github/codeql/codeql-android-critical-security.yml
- name: Build Android for CodeQL
working-directory: apps/android
run: ./gradlew --no-daemon :app:assemblePlayDebug
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-security/android"

View File

@@ -1,619 +0,0 @@
name: CodeQL Critical Quality
on:
workflow_dispatch:
inputs:
profile:
description: CodeQL quality profile to run
required: false
default: all
type: choice
options:
- all
- agent-runtime-boundary
- config-boundary
- core-auth-secrets
- channel-runtime-boundary
- gateway-runtime-boundary
- memory-runtime-boundary
- mcp-process-runtime-boundary
- plugin-boundary
- plugin-sdk-package-contract
- plugin-sdk-reply-runtime
- provider-runtime-boundary
- session-diagnostics-boundary
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- ".github/codeql/**"
- ".github/workflows/codeql-critical-quality.yml"
- "packages/plugin-package-contract/**"
- "packages/plugin-sdk/**"
- "packages/memory-host-sdk/**"
- "src/config/**"
- "extensions/bluebubbles/src/**"
- "extensions/discord/src/**"
- "extensions/feishu/src/**"
- "extensions/googlechat/src/**"
- "extensions/imessage/src/**"
- "extensions/irc/src/**"
- "extensions/line/src/**"
- "extensions/matrix/src/**"
- "extensions/mattermost/src/**"
- "extensions/msteams/src/**"
- "extensions/nextcloud-talk/src/**"
- "extensions/nostr/src/**"
- "extensions/qa-channel/src/**"
- "extensions/qqbot/src/**"
- "extensions/signal/src/**"
- "extensions/slack/src/**"
- "extensions/synology-chat/src/**"
- "extensions/telegram/src/**"
- "extensions/tlon/src/**"
- "extensions/twitch/src/**"
- "extensions/whatsapp/src/**"
- "extensions/zalo/src/**"
- "extensions/zalouser/src/**"
- "src/agents/*auth*.ts"
- "src/agents/**/*auth*.ts"
- "src/agents/auth-health*.ts"
- "src/agents/auth-profiles"
- "src/agents/auth-profiles/**"
- "src/agents/bash-tools.exec-host-shared.ts"
- "src/agents/sandbox"
- "src/agents/sandbox/**"
- "src/agents/sandbox.ts"
- "src/agents/sandbox-*.ts"
- "src/acp/control-plane/**"
- "src/agents/cli-runner/**"
- "src/agents/command/**"
- "src/agents/pi-embedded-runner/**"
- "src/agents/tools/**"
- "src/agents/*completion*.ts"
- "src/agents/*transport*.ts"
- "src/agents/model-*.ts"
- "src/agents/openclaw-tools*.ts"
- "src/agents/provider-*.ts"
- "src/agents/session*.ts"
- "src/agents/tool-call*.ts"
- "src/auto-reply/reply/agent-runner*.ts"
- "src/auto-reply/reply/commands*.ts"
- "src/auto-reply/reply/directive-handling*.ts"
- "src/auto-reply/reply/dispatch-*.ts"
- "src/auto-reply/reply/get-reply-run*.ts"
- "src/auto-reply/reply/provider-dispatcher*.ts"
- "src/auto-reply/reply/queue*.ts"
- "src/auto-reply/reply/reply-run-registry*.ts"
- "src/auto-reply/reply/session*.ts"
- "src/channels/**"
- "src/auto-reply/reply/post-compaction-context.ts"
- "src/auto-reply/reply/queue/**"
- "src/auto-reply/reply/startup-context.ts"
- "src/commands/doctor-cron-dreaming-payload-migration.ts"
- "src/commands/doctor-memory-search.ts"
- "src/commands/doctor-session-*.ts"
- "src/commands/session-store-targets.ts"
- "src/commands/sessions*.ts"
- "src/cron/service/jobs.ts"
- "src/cron/stagger.ts"
- "src/gateway/*auth*.ts"
- "src/gateway/**/*auth*.ts"
- "src/gateway/*secret*.ts"
- "src/gateway/**/*secret*.ts"
- "src/gateway/protocol/**/*secret*.ts"
- "src/gateway/resolve-configured-secret-input-string*.ts"
- "src/gateway/security-path*.ts"
- "src/gateway/server-methods/secrets*.ts"
- "src/gateway/server-startup-memory.ts"
- "src/gateway/method-scopes.ts"
- "src/gateway/protocol/**"
- "src/gateway/server-methods/**"
- "src/gateway/server-methods.ts"
- "src/gateway/server-methods-list.ts"
- "src/infra/diagnostic-*.ts"
- "src/infra/diagnostics-timeline.ts"
- "src/infra/outbound/**"
- "src/infra/secret-file*.ts"
- "src/infra/session-delivery-queue*.ts"
- "src/logging/diagnostic*.ts"
- "src/memory/**"
- "src/memory-host-sdk/**"
- "src/mcp/**"
- "src/model-catalog/**"
- "src/plugin-sdk/**"
- "src/plugins/**"
- "src/process/**"
- "src/secrets/**"
- "src/security/**"
schedule:
- cron: "30 6 * * *"
concurrency:
group: codeql-critical-quality-${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.event_name == 'pull_request' && github.event.pull_request.number || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
permissions:
actions: read
contents: read
pull-requests: read
security-events: write
jobs:
quality-shards:
name: Select Critical Quality shards
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 5
outputs:
agent: ${{ steps.detect.outputs.agent }}
channel: ${{ steps.detect.outputs.channel }}
config: ${{ steps.detect.outputs.config }}
core_auth_secrets: ${{ steps.detect.outputs.core_auth_secrets }}
gateway: ${{ steps.detect.outputs.gateway }}
memory: ${{ steps.detect.outputs.memory }}
mcp_process: ${{ steps.detect.outputs.mcp_process }}
plugin: ${{ steps.detect.outputs.plugin }}
plugin_sdk_package: ${{ steps.detect.outputs.plugin_sdk_package }}
plugin_sdk_reply: ${{ steps.detect.outputs.plugin_sdk_reply }}
provider: ${{ steps.detect.outputs.provider }}
session_diagnostics: ${{ steps.detect.outputs.session_diagnostics }}
steps:
- name: Detect PR shard paths
id: detect
env:
EVENT_NAME: ${{ github.event_name }}
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPOSITORY: ${{ github.repository }}
run: |
set -euo pipefail
agent=false
channel=false
config=false
core_auth_secrets=false
gateway=false
memory=false
mcp_process=false
plugin=false
plugin_sdk_package=false
plugin_sdk_reply=false
provider=false
session_diagnostics=false
if [[ "${EVENT_NAME}" != "pull_request" ]]; then
agent=true
channel=true
config=true
core_auth_secrets=true
gateway=true
memory=true
mcp_process=true
plugin=true
plugin_sdk_package=true
plugin_sdk_reply=true
provider=true
session_diagnostics=true
else
while IFS= read -r file; do
case "${file}" in
.github/codeql/*|.github/workflows/codeql-critical-quality.yml)
agent=true
channel=true
config=true
core_auth_secrets=true
gateway=true
memory=true
mcp_process=true
plugin=true
plugin_sdk_package=true
plugin_sdk_reply=true
provider=true
session_diagnostics=true
;;
src/acp/control-plane/*|src/agents/cli-runner/*|src/agents/command/*|src/agents/pi-embedded-runner/*|src/agents/tools/*|src/agents/*completion*.ts|src/agents/*transport*.ts|src/agents/model-*.ts|src/agents/openclaw-tools*.ts|src/agents/provider-*.ts|src/agents/session*.ts|src/agents/tool-call*.ts|src/auto-reply/reply/agent-runner*.ts|src/auto-reply/reply/commands*.ts|src/auto-reply/reply/directive-handling*.ts|src/auto-reply/reply/dispatch-*.ts|src/auto-reply/reply/get-reply-run*.ts|src/auto-reply/reply/provider-dispatcher*.ts|src/auto-reply/reply/queue*.ts|src/auto-reply/reply/reply-run-registry*.ts|src/auto-reply/reply/session*.ts)
agent=true
;;
src/auto-reply/reply/post-compaction-context.ts|src/auto-reply/reply/queue/*|src/auto-reply/reply/startup-context.ts|src/commands/doctor-session-*.ts|src/commands/session-store-targets.ts|src/commands/sessions*.ts|src/infra/diagnostic-*.ts|src/infra/diagnostics-timeline.ts|src/infra/session-delivery-queue*.ts|src/logging/diagnostic*.ts)
session_diagnostics=true
;;
extensions/bluebubbles/src/*|extensions/discord/src/*|extensions/feishu/src/*|extensions/googlechat/src/*|extensions/imessage/src/*|extensions/irc/src/*|extensions/line/src/*|extensions/matrix/src/*|extensions/mattermost/src/*|extensions/msteams/src/*|extensions/nextcloud-talk/src/*|extensions/nostr/src/*|extensions/qa-channel/src/*|extensions/qqbot/src/*|extensions/signal/src/*|extensions/slack/src/*|extensions/synology-chat/src/*|extensions/telegram/src/*|extensions/tlon/src/*|extensions/twitch/src/*|extensions/whatsapp/src/*|extensions/zalo/src/*|extensions/zalouser/src/*|src/channels/*)
channel=true
;;
src/config/*)
config=true
;;
src/gateway/protocol/*secret*.ts|src/gateway/server-methods/secrets*.ts)
core_auth_secrets=true
gateway=true
;;
src/agents/*auth*.ts|src/agents/auth-health*.ts|src/agents/auth-profiles|src/agents/auth-profiles/*|src/agents/bash-tools.exec-host-shared.ts|src/agents/sandbox|src/agents/sandbox.ts|src/agents/sandbox-*.ts|src/agents/sandbox/*|src/cron/service/jobs.ts|src/cron/stagger.ts|src/gateway/*auth*.ts|src/gateway/*secret*.ts|src/gateway/resolve-configured-secret-input-string*.ts|src/gateway/security-path*.ts|src/infra/secret-file*.ts|src/secrets/*|src/security/*)
core_auth_secrets=true
;;
src/gateway/method-scopes.ts|src/gateway/protocol/*|src/gateway/server-methods/*|src/gateway/server-methods.ts|src/gateway/server-methods-list.ts)
gateway=true
;;
packages/memory-host-sdk/*|src/commands/doctor-cron-dreaming-payload-migration.ts|src/commands/doctor-memory-search.ts|src/gateway/server-startup-memory.ts|src/memory/*|src/memory-host-sdk/*)
memory=true
;;
src/infra/outbound/base-session-key.ts|src/infra/outbound/delivery-queue*.ts|src/infra/outbound/outbound-session.ts|src/infra/outbound/session-binding*.ts|src/infra/outbound/session-context.ts|src/infra/outbound/targets-session.ts)
mcp_process=true
session_diagnostics=true
;;
src/infra/outbound/*|src/mcp/*|src/process/*)
mcp_process=true
;;
src/plugin-sdk/inbound-envelope.ts|src/plugin-sdk/inbound-reply-dispatch.ts|src/plugin-sdk/reply-*.ts|src/plugin-sdk/channel-reply-*.ts|src/plugin-sdk/delivery-queue-runtime.ts|src/plugin-sdk/outbound-runtime.ts|src/plugin-sdk/outbound-send-deps.ts|src/plugin-sdk/model-session-runtime.ts|src/plugin-sdk/session-*.ts|src/plugin-sdk/thread-bindings-runtime.ts|src/plugin-sdk/thread-bindings-session-runtime.ts|src/plugin-sdk/conversation-binding-runtime.ts)
plugin=true
plugin_sdk_package=true
plugin_sdk_reply=true
;;
src/plugin-sdk/memory-*.ts|src/plugin-sdk/memory-core-host-*.ts)
memory=true
plugin=true
plugin_sdk_package=true
;;
src/plugin-sdk/*)
plugin=true
plugin_sdk_package=true
;;
src/plugins/provider-contract-public-artifacts.ts|src/plugins/provider-public-artifacts.ts|src/plugins/web-provider-public-artifacts*.ts)
plugin=true
provider=true
;;
src/plugins/memory-embedding-provider*.ts|src/plugins/memory-embedding-providers*.ts)
memory=true
provider=true
;;
src/plugins/memory-*.ts)
memory=true
;;
src/model-catalog/*|src/plugins/*provider*.ts|src/plugins/capability-provider-runtime.ts|src/plugins/compaction-provider.ts|src/plugins/memory-embedding-provider*.ts|src/plugins/memory-embedding-providers*.ts|src/plugins/migration-provider-runtime.ts|src/plugins/synthetic-auth.runtime.ts|src/plugins/web-fetch-providers*.ts|src/plugins/web-search-providers*.ts)
provider=true
;;
src/plugins/activation-planner.ts|src/plugins/api-builder.ts|src/plugins/bundled-*.ts|src/plugins/captured-registration.ts|src/plugins/config-*.ts|src/plugins/discovery.ts|src/plugins/effective-plugin-ids.ts|src/plugins/externalized-bundled-plugins.ts|src/plugins/installed-plugin-index*.ts|src/plugins/loader*.ts|src/plugins/manifest*.ts|src/plugins/module-export.ts|src/plugins/package-entrypoints.ts|src/plugins/plugin-registry*.ts|src/plugins/public-surface*.ts|src/plugins/registry.ts|src/plugins/registry-types.ts|src/plugins/runtime|src/plugins/runtime/*|src/plugins/runtime-state.ts|src/plugins/runtime.ts|src/plugins/sdk-alias.ts|src/plugins/source-loader.ts|src/plugins/types.ts|src/plugins/validation-diagnostics.ts)
plugin=true
;;
packages/plugin-package-contract/*|packages/plugin-sdk/*)
plugin_sdk_package=true
;;
esac
done < <(gh api --paginate "repos/${REPOSITORY}/pulls/${PR_NUMBER}/files" --jq '.[].filename')
fi
{
echo "agent=${agent}"
echo "channel=${channel}"
echo "config=${config}"
echo "core_auth_secrets=${core_auth_secrets}"
echo "gateway=${gateway}"
echo "memory=${memory}"
echo "mcp_process=${mcp_process}"
echo "plugin=${plugin}"
echo "plugin_sdk_package=${plugin_sdk_package}"
echo "plugin_sdk_reply=${plugin_sdk_reply}"
echo "provider=${provider}"
echo "session_diagnostics=${session_diagnostics}"
} >> "${GITHUB_OUTPUT}"
core-auth-secrets:
name: Critical Quality (core-auth-secrets)
needs: quality-shards
if: ${{ needs.quality-shards.outputs.core_auth_secrets == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'core-auth-secrets') }}
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-core-auth-secrets-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-quality/core-auth-secrets"
config-boundary:
name: Critical Quality (config-boundary)
needs: quality-shards
if: ${{ needs.quality-shards.outputs.config == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'config-boundary') }}
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-config-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-quality/config-boundary"
gateway-runtime-boundary:
name: Critical Quality (gateway-runtime-boundary)
needs: quality-shards
if: ${{ needs.quality-shards.outputs.gateway == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'gateway-runtime-boundary') }}
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-gateway-runtime-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-quality/gateway-runtime-boundary"
channel-runtime-boundary:
name: Critical Quality (channel-runtime-boundary)
needs: quality-shards
if: ${{ needs.quality-shards.outputs.channel == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'channel-runtime-boundary') }}
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-channel-runtime-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-quality/channel-runtime-boundary"
agent-runtime-boundary:
name: Critical Quality (agent-runtime-boundary)
needs: quality-shards
if: ${{ needs.quality-shards.outputs.agent == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'agent-runtime-boundary') }}
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-agent-runtime-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-quality/agent-runtime-boundary"
mcp-process-runtime-boundary:
name: Critical Quality (mcp-process-runtime-boundary)
needs: quality-shards
if: ${{ needs.quality-shards.outputs.mcp_process == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'mcp-process-runtime-boundary') }}
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-mcp-process-runtime-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-quality/mcp-process-runtime-boundary"
memory-runtime-boundary:
name: Critical Quality (memory-runtime-boundary)
needs: quality-shards
if: ${{ needs.quality-shards.outputs.memory == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'memory-runtime-boundary') }}
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-memory-runtime-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-quality/memory-runtime-boundary"
session-diagnostics-boundary:
name: Critical Quality (session-diagnostics-boundary)
needs: quality-shards
if: ${{ needs.quality-shards.outputs.session_diagnostics == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'session-diagnostics-boundary') }}
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-session-diagnostics-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-quality/session-diagnostics-boundary"
plugin-sdk-reply-runtime:
name: Critical Quality (plugin-sdk-reply-runtime)
needs: quality-shards
if: ${{ needs.quality-shards.outputs.plugin_sdk_reply == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'plugin-sdk-reply-runtime') }}
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-plugin-sdk-reply-runtime-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-quality/plugin-sdk-reply-runtime"
provider-runtime-boundary:
name: Critical Quality (provider-runtime-boundary)
needs: quality-shards
if: ${{ needs.quality-shards.outputs.provider == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'provider-runtime-boundary') }}
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-provider-runtime-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-quality/provider-runtime-boundary"
ui-control-plane:
name: Critical Quality (ui-control-plane)
if: ${{ github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || inputs.profile == 'all') }}
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-ui-control-plane-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-quality/ui-control-plane"
web-media-runtime-boundary:
name: Critical Quality (web-media-runtime-boundary)
if: ${{ github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || inputs.profile == 'all') }}
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-web-media-runtime-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-quality/web-media-runtime-boundary"
plugin-boundary:
name: Critical Quality (plugin-boundary)
needs: quality-shards
if: ${{ needs.quality-shards.outputs.plugin == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'plugin-boundary') }}
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-plugin-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-quality/plugin-boundary"
plugin-sdk-package-contract:
name: Critical Quality (plugin-sdk-package-contract)
needs: quality-shards
if: ${{ needs.quality-shards.outputs.plugin_sdk_package == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'plugin-sdk-package-contract') }}
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-plugin-sdk-package-contract-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-quality/plugin-sdk-package-contract"

View File

@@ -1,89 +0,0 @@
name: CodeQL macOS Critical Security
on:
workflow_dispatch:
schedule:
- cron: "0 8 * * 1"
concurrency:
group: codeql-macos-critical-security-${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.sha }}
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
permissions:
actions: read
contents: read
security-events: write
jobs:
macos:
name: Critical Security (macOS)
runs-on: blacksmith-6vcpu-macos-latest
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
submodules: false
- name: Select Xcode
run: |
sudo xcode-select -s /Applications/Xcode_26.1.app
xcodebuild -version
swift --version
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: swift
build-mode: manual
config-file: ./.github/codeql/codeql-macos-critical-security.yml
- name: Build macOS for CodeQL
run: swift build --package-path apps/macos --product OpenClaw
- name: Analyze
id: analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
output: sarif-results
upload: failure-only
category: "/codeql-critical-security/macos"
- name: Remove dependency build results
env:
SARIF_OUTPUT: sarif-results
run: |
set -euo pipefail
shopt -s nullglob
if [ ! -d "$SARIF_OUTPUT" ]; then
echo "SARIF output directory not found: $SARIF_OUTPUT" >&2
exit 1
fi
mkdir -p sarif-results-filtered
files=("$SARIF_OUTPUT"/*.sarif)
if [ "${#files[@]}" -eq 0 ]; then
echo "No SARIF files found in $SARIF_OUTPUT" >&2
exit 1
fi
for file in "${files[@]}"; do
jq '
def in_dependency_build:
((.locations // []) | length > 0)
and all(.locations[]; (.physicalLocation.artifactLocation.uri? // "") | test("^apps/macos/\\.build/"));
.runs |= map(.results = ((.results // []) | map(select(in_dependency_build | not))))
' "$file" > "sarif-results-filtered/$(basename "$file")"
done
- name: Upload filtered SARIF
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
sarif_file: sarif-results-filtered
category: "/codeql-critical-security/macos"

View File

@@ -2,28 +2,11 @@ name: CodeQL
on:
workflow_dispatch:
inputs:
profile:
description: CodeQL security profile to run
required: false
default: all
type: choice
options:
- all
- security
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- ".github/actions/**"
- ".github/codeql/**"
- ".github/workflows/**"
- "packages/**"
- "src/**"
schedule:
- cron: "0 6 * * *"
concurrency:
group: codeql-${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.event_name == 'pull_request' && github.event.pull_request.number || github.sha }}
group: codeql-${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
@@ -35,58 +18,121 @@ permissions:
security-events: write
jobs:
security-high:
name: Security High (${{ matrix.category }})
if: ${{ (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'security') }}
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ${{ matrix.runs_on }}
timeout-minutes: ${{ matrix.timeout_minutes }}
strategy:
fail-fast: false
matrix:
include:
- language: javascript-typescript
category: core-auth-secrets
runs_on: blacksmith-8vcpu-ubuntu-2404
timeout_minutes: 25
config_file: ./.github/codeql/codeql-core-auth-secrets-critical-security.yml
- language: javascript-typescript
category: channel-runtime-boundary
runs_on: blacksmith-8vcpu-ubuntu-2404
timeout_minutes: 25
config_file: ./.github/codeql/codeql-channel-runtime-boundary-critical-security.yml
- language: javascript-typescript
category: network-ssrf-boundary
runs_on: blacksmith-4vcpu-ubuntu-2404
timeout_minutes: 25
config_file: ./.github/codeql/codeql-network-ssrf-boundary-critical-security.yml
- language: javascript-typescript
category: mcp-process-tool-boundary
runs_on: blacksmith-4vcpu-ubuntu-2404
timeout_minutes: 25
config_file: ./.github/codeql/codeql-mcp-process-tool-boundary-critical-security.yml
- language: javascript-typescript
category: plugin-trust-boundary
runs_on: blacksmith-4vcpu-ubuntu-2404
timeout_minutes: 25
config_file: ./.github/codeql/codeql-plugin-trust-boundary-critical-security.yml
runs_on: blacksmith-32vcpu-ubuntu-2404
needs_node: true
needs_python: false
needs_java: false
needs_swift_tools: false
needs_manual_build: false
needs_autobuild: false
config_file: ./.github/codeql/codeql-javascript-typescript.yml
- language: actions
category: actions
runs_on: blacksmith-8vcpu-ubuntu-2404
timeout_minutes: 10
config_file: ./.github/codeql/codeql-actions-critical-security.yml
runs_on: blacksmith-16vcpu-ubuntu-2404
needs_node: false
needs_python: false
needs_java: false
needs_swift_tools: false
needs_manual_build: false
needs_autobuild: false
config_file: ""
- language: python
runs_on: blacksmith-16vcpu-ubuntu-2404
needs_node: false
needs_python: true
needs_java: false
needs_swift_tools: false
needs_manual_build: false
needs_autobuild: false
config_file: ""
- language: java-kotlin
runs_on: blacksmith-16vcpu-ubuntu-2404
needs_node: false
needs_python: false
needs_java: true
needs_swift_tools: false
needs_manual_build: true
needs_autobuild: false
config_file: ""
- language: swift
runs_on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-12vcpu-macos-latest' || 'macos-latest' }}
needs_node: false
needs_python: false
needs_java: false
needs_swift_tools: true
needs_manual_build: true
needs_autobuild: false
config_file: ""
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
submodules: false
- name: Setup Node environment
if: matrix.needs_node
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
- name: Setup Python
if: matrix.needs_python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.12"
- name: Setup Java
if: matrix.needs_java
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
distribution: temurin
java-version: "21"
- name: Setup Swift build tools
if: matrix.needs_swift_tools
run: |
sudo xcode-select -s /Applications/Xcode_26.1.app
xcodebuild -version
brew install xcodegen swiftlint swiftformat
swift --version
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/init@b25d0ebf40e5b63ee81e1bd6e5d2a12b7c2aeb61 # v4
with:
languages: ${{ matrix.language }}
config-file: ${{ matrix.config_file }}
queries: security-and-quality
config-file: ${{ matrix.config_file || '' }}
- name: Autobuild
if: matrix.needs_autobuild
uses: github/codeql-action/autobuild@b25d0ebf40e5b63ee81e1bd6e5d2a12b7c2aeb61 # v4
- name: Build Android for CodeQL
if: matrix.language == 'java-kotlin'
working-directory: apps/android
run: ./gradlew --no-daemon :app:assemblePlayDebug
- name: Build Swift for CodeQL
if: matrix.language == 'swift'
run: |
set -euo pipefail
swift build --package-path apps/macos --configuration release
cd apps/ios
xcodegen generate
xcodebuild build \
-project OpenClaw.xcodeproj \
-scheme OpenClaw \
-destination "generic/platform=iOS Simulator" \
CODE_SIGNING_ALLOWED=NO
- name: Analyze
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/analyze@b25d0ebf40e5b63ee81e1bd6e5d2a12b7c2aeb61 # v4
with:
category: "/codeql-security-high/${{ matrix.category }}"
category: "/language:${{ matrix.language }}"

View File

@@ -49,7 +49,7 @@ jobs:
run: |
set -euo pipefail
all_locales_json='["zh-CN","zh-TW","pt-BR","de","es","ja-JP","ko","fr","ar","it","tr","uk","id","pl","th","vi","nl","fa"]'
all_locales_json='["zh-CN","zh-TW","pt-BR","de","es","ja-JP","ko","fr","tr","uk","id","pl","th"]'
if [ "$EVENT_NAME" != "push" ]; then
echo "has_locales=true" >> "$GITHUB_OUTPUT"

View File

@@ -1,145 +0,0 @@
name: Crabbox Hydrate
on:
workflow_dispatch:
inputs:
crabbox_id:
description: "Crabbox lease ID"
required: true
type: string
ref:
description: "Git ref to hydrate"
required: false
type: string
crabbox_runner_label:
description: "Dynamic Crabbox runner label"
required: true
type: string
crabbox_job:
description: "Hydration job identifier expected by Crabbox"
required: false
default: "hydrate"
type: string
crabbox_keep_alive_minutes:
description: "Minutes to keep the hydrated job alive"
required: false
default: "90"
type: string
permissions:
contents: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
hydrate:
name: hydrate
runs-on: [self-hosted, "${{ inputs.crabbox_runner_label }}"]
timeout-minutes: 120
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
- name: Prepare Crabbox shell
shell: bash
run: |
set -euo pipefail
git fetch --no-tags --depth=50 origin "+refs/heads/main:refs/remotes/origin/main"
node_bin="$(dirname "$(node -p 'process.execPath')")"
pnpm_bin="$(command -v pnpm)"
sudo ln -sf "$node_bin/node" /usr/local/bin/node
sudo ln -sf "$node_bin/npm" /usr/local/bin/npm
sudo ln -sf "$node_bin/npx" /usr/local/bin/npx
sudo ln -sf "$node_bin/corepack" /usr/local/bin/corepack
sudo ln -sf "$pnpm_bin" /usr/local/bin/pnpm
- name: Hydrate provider env helper
shell: bash
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
run: bash scripts/ci-hydrate-testbox-env.sh
- name: Mark Crabbox ready
shell: bash
run: |
set -euo pipefail
job="${{ inputs.crabbox_job }}"
if [ -z "$job" ]; then job=hydrate; fi
mkdir -p "$HOME/.crabbox/actions"
state="$HOME/.crabbox/actions/${{ inputs.crabbox_id }}.env"
env_file="$HOME/.crabbox/actions/${{ inputs.crabbox_id }}.env.sh"
services_file="$HOME/.crabbox/actions/${{ inputs.crabbox_id }}.services"
write_export() {
key="$1"
value="${!key-}"
if [ -n "$value" ]; then
printf 'export %s=%q\n' "$key" "$value"
fi
}
{
for key in CI GITHUB_ACTIONS GITHUB_WORKSPACE GITHUB_REPOSITORY GITHUB_RUN_ID GITHUB_RUN_NUMBER GITHUB_RUN_ATTEMPT GITHUB_REF GITHUB_REF_NAME GITHUB_SHA GITHUB_EVENT_NAME GITHUB_ACTOR RUNNER_OS RUNNER_ARCH RUNNER_TEMP RUNNER_TOOL_CACHE; do
write_export "$key"
done
} > "${env_file}.tmp"
mv "${env_file}.tmp" "$env_file"
{
echo "# Docker containers visible from the hydrated runner"
docker ps --format '{{.Names}}\t{{.Image}}\t{{.Ports}}' 2>/dev/null || true
} > "${services_file}.tmp"
mv "${services_file}.tmp" "$services_file"
tmp="${state}.tmp"
{
echo "WORKSPACE=${GITHUB_WORKSPACE}"
echo "RUN_ID=${GITHUB_RUN_ID}"
echo "JOB=${job}"
echo "ENV_FILE=${env_file}"
echo "SERVICES_FILE=${services_file}"
echo "READY_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
} > "$tmp"
mv "$tmp" "$state"
- name: Keep Crabbox job alive
shell: bash
run: |
set -euo pipefail
minutes="${{ inputs.crabbox_keep_alive_minutes }}"
case "$minutes" in
''|*[!0-9]*) minutes=90 ;;
esac
stop="$HOME/.crabbox/actions/${{ inputs.crabbox_id }}.stop"
deadline=$(( $(date +%s) + minutes * 60 ))
while [ "$(date +%s)" -lt "$deadline" ]; do
if [ -f "$stop" ]; then
exit 0
fi
sleep 15
done

View File

@@ -55,7 +55,6 @@ jobs:
# WARNING: KEEP MANUAL BACKFILLS GATED BY THE docker-release ENVIRONMENT.
runs-on: ubuntu-24.04
environment: docker-release
permissions: {}
steps:
- name: Approve Docker backfill
env:

View File

@@ -149,7 +149,7 @@ jobs:
- name: Run Codex docs agent
if: steps.gate.outputs.run_agent == 'true'
uses: openai/codex-action@5c3f4ccdb2b8790f73d6b21751ac00e602aa0c02
uses: openai/codex-action@v1
env:
DOCS_AGENT_BASE_SHA: ${{ steps.gate.outputs.review_base_sha }}
DOCS_AGENT_HEAD_SHA: ${{ steps.gate.outputs.review_head_sha }}

View File

@@ -20,7 +20,6 @@ jobs:
set -euo pipefail
for event_type in \
translate-zh-cn-release \
translate-zh-tw-release \
translate-ja-jp-release \
translate-es-release \
translate-pt-br-release \
@@ -29,9 +28,6 @@ jobs:
translate-fr-release \
translate-ar-release \
translate-it-release \
translate-vi-release \
translate-nl-release \
translate-fa-release \
translate-tr-release \
translate-uk-release \
translate-id-release \

View File

@@ -1,785 +0,0 @@
name: Full Release Validation
on:
workflow_dispatch:
inputs:
ref:
description: Branch, tag, or full commit SHA to validate
required: true
default: main
type: string
provider:
description: Provider lane for cross-OS onboarding and the end-to-end agent turn
required: false
default: openai
type: choice
options:
- openai
- anthropic
- minimax
mode:
description: Which cross-OS release lanes to run
required: false
default: both
type: choice
options:
- fresh
- upgrade
- both
release_profile:
description: Release coverage profile for live/Docker/provider breadth
required: false
default: stable
type: choice
options:
- minimum
- stable
- full
rerun_group:
description: Validation group to run
required: false
default: all
type: choice
options:
- all
- ci
- plugin-prerelease
- release-checks
- install-smoke
- cross-os
- live-e2e
- package
- qa
- qa-parity
- qa-live
- npm-telegram
live_suite_filter:
description: Optional exact live suite id for focused live/E2E reruns; blank runs all selected live suites
required: false
default: ""
type: string
npm_telegram_package_spec:
description: Optional published package spec for the post-publish Telegram E2E lane
required: false
default: ""
type: string
evidence_package_spec:
description: Optional published package spec to prove in the private release evidence report
required: false
default: ""
type: string
npm_telegram_provider_mode:
description: Provider mode for the optional post-publish Telegram E2E lane
required: false
default: mock-openai
type: choice
options:
- mock-openai
- live-frontier
npm_telegram_scenario:
description: Optional comma-separated Telegram scenario ids for the post-publish lane
required: false
default: ""
type: string
permissions:
actions: write
contents: read
concurrency:
group: full-release-validation-${{ inputs.ref }}-${{ inputs.rerun_group }}
cancel-in-progress: ${{ inputs.ref == 'main' && inputs.rerun_group == 'all' }}
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
GH_REPO: ${{ github.repository }}
jobs:
resolve_target:
name: Resolve target ref
runs-on: ubuntu-24.04
timeout-minutes: 10
outputs:
sha: ${{ steps.resolve.outputs.sha }}
steps:
- name: Checkout trusted workflow helper
uses: actions/checkout@v6
with:
ref: ${{ github.ref_name }}
path: workflow
fetch-depth: 1
persist-credentials: false
submodules: false
- name: Resolve target SHA
id: resolve
env:
TARGET_REF: ${{ inputs.ref }}
run: |
bash workflow/scripts/github/resolve-openclaw-ref.sh \
--ref "$TARGET_REF" \
--github-output "$GITHUB_OUTPUT"
- name: Summarize target
env:
TARGET_REF: ${{ inputs.ref }}
TARGET_SHA: ${{ steps.resolve.outputs.sha }}
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
NPM_TELEGRAM_PACKAGE_SPEC: ${{ inputs.npm_telegram_package_spec }}
EVIDENCE_PACKAGE_SPEC: ${{ inputs.evidence_package_spec }}
RERUN_GROUP: ${{ inputs.rerun_group }}
LIVE_SUITE_FILTER: ${{ inputs.live_suite_filter }}
run: |
{
echo "## Full release validation"
echo
echo "- Target ref: \`${TARGET_REF}\`"
echo "- Target SHA: \`${TARGET_SHA}\`"
echo "- Child workflow ref: \`${CHILD_WORKFLOW_REF}\`"
echo "- Rerun group: \`${RERUN_GROUP}\`"
if [[ -n "${LIVE_SUITE_FILTER// }" ]]; then
echo "- Live suite filter: \`${LIVE_SUITE_FILTER}\`"
fi
if [[ "$RERUN_GROUP" == "all" || "$RERUN_GROUP" == "ci" ]]; then
echo "- Normal CI: \`CI\` with \`target_ref=${TARGET_SHA}\`"
else
echo "- Normal CI: skipped by rerun group"
fi
if [[ "$RERUN_GROUP" == "all" || "$RERUN_GROUP" == "plugin-prerelease" ]]; then
echo "- Plugin prerelease: \`Plugin Prerelease\` with \`target_ref=${TARGET_SHA}\`"
else
echo "- Plugin prerelease: skipped by rerun group"
fi
if [[ "$RERUN_GROUP" == "all" || "$RERUN_GROUP" == "release-checks" || "$RERUN_GROUP" == "install-smoke" || "$RERUN_GROUP" == "cross-os" || "$RERUN_GROUP" == "live-e2e" || "$RERUN_GROUP" == "package" || "$RERUN_GROUP" == "qa" || "$RERUN_GROUP" == "qa-parity" || "$RERUN_GROUP" == "qa-live" ]]; then
echo "- Release/live/Docker/package/QA: \`OpenClaw Release Checks\`"
else
echo "- Release/live/Docker/package/QA: skipped by rerun group"
fi
if [[ -n "${NPM_TELEGRAM_PACKAGE_SPEC// }" ]]; then
echo "- Post-publish Telegram E2E: \`${NPM_TELEGRAM_PACKAGE_SPEC}\`"
else
echo "- Post-publish Telegram E2E: skipped because no published package spec was provided"
fi
if [[ -n "${EVIDENCE_PACKAGE_SPEC// }" ]]; then
echo "- Private evidence package proof: \`${EVIDENCE_PACKAGE_SPEC}\`"
fi
} >> "$GITHUB_STEP_SUMMARY"
normal_ci:
name: Run normal full CI
needs: [resolve_target]
if: contains(fromJSON('["all","ci"]'), inputs.rerun_group)
runs-on: ubuntu-24.04
timeout-minutes: 240
outputs:
run_id: ${{ steps.dispatch.outputs.run_id }}
url: ${{ steps.dispatch.outputs.url }}
conclusion: ${{ steps.dispatch.outputs.conclusion }}
steps:
- name: Dispatch and monitor CI
id: dispatch
env:
GH_TOKEN: ${{ github.token }}
TARGET_REF: ${{ inputs.ref }}
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
run: |
set -euo pipefail
dispatch_and_wait() {
local workflow="$1"
shift
local before_json dispatch_output run_id status conclusion url
before_json="$(gh run list --workflow "$workflow" --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
dispatch_output="$(gh workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1)"
printf '%s\n' "$dispatch_output"
run_id="$(
printf '%s\n' "$dispatch_output" |
sed -nE 's#.*actions/runs/([0-9]+).*#\1#p' |
tail -n 1
)"
if [[ -z "$run_id" ]]; then
for _ in $(seq 1 60); do
run_id="$(
BEFORE_IDS="$before_json" gh run list --workflow "$workflow" --event workflow_dispatch --limit 50 --json databaseId,createdAt \
--jq 'map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty'
)"
if [[ -n "$run_id" ]]; then
break
fi
sleep 5
done
fi
if [[ -z "${run_id:-}" ]]; then
echo "Could not find dispatched run for ${workflow}." >&2
exit 1
fi
echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"
cancel_child() {
if [[ -n "${run_id:-}" ]]; then
echo "Cancelling child workflow ${workflow}: ${run_id}" >&2
gh run cancel "$run_id" >/dev/null 2>&1 || true
fi
}
trap cancel_child EXIT INT TERM
while true; do
status="$(gh run view "$run_id" --json status --jq '.status')"
if [[ "$status" == "completed" ]]; then
break
fi
sleep 30
done
trap - EXIT INT TERM
conclusion="$(gh run view "$run_id" --json conclusion --jq '.conclusion')"
url="$(gh run view "$run_id" --json url --jq '.url')"
echo "${workflow} finished with ${conclusion}: ${url}"
echo "url=${url}" >> "$GITHUB_OUTPUT"
echo "conclusion=${conclusion}" >> "$GITHUB_OUTPUT"
if [[ "$conclusion" != "success" ]]; then
gh run view "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' || true
fi
}
{
echo "### Normal CI"
echo
echo "- Target ref: \`${TARGET_REF}\`"
echo "- Target SHA: \`${TARGET_SHA}\`"
} >> "$GITHUB_STEP_SUMMARY"
dispatch_and_wait ci.yml -f target_ref="$TARGET_SHA" -f include_android=true
plugin_prerelease:
name: Run plugin prerelease validation
needs: [resolve_target]
if: contains(fromJSON('["all","plugin-prerelease"]'), inputs.rerun_group)
runs-on: ubuntu-24.04
timeout-minutes: 300
outputs:
run_id: ${{ steps.dispatch.outputs.run_id }}
url: ${{ steps.dispatch.outputs.url }}
conclusion: ${{ steps.dispatch.outputs.conclusion }}
steps:
- name: Dispatch and monitor plugin prerelease
id: dispatch
env:
GH_TOKEN: ${{ github.token }}
TARGET_REF: ${{ inputs.ref }}
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
run: |
set -euo pipefail
dispatch_and_wait() {
local workflow="$1"
shift
local before_json dispatch_output run_id status conclusion url
before_json="$(gh run list --workflow "$workflow" --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
dispatch_output="$(gh workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1)"
printf '%s\n' "$dispatch_output"
run_id="$(
printf '%s\n' "$dispatch_output" |
sed -nE 's#.*actions/runs/([0-9]+).*#\1#p' |
tail -n 1
)"
if [[ -z "$run_id" ]]; then
for _ in $(seq 1 60); do
run_id="$(
BEFORE_IDS="$before_json" gh run list --workflow "$workflow" --event workflow_dispatch --limit 50 --json databaseId,createdAt \
--jq 'map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty'
)"
if [[ -n "$run_id" ]]; then
break
fi
sleep 5
done
fi
if [[ -z "${run_id:-}" ]]; then
echo "Could not find dispatched run for ${workflow}." >&2
exit 1
fi
echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"
cancel_child() {
if [[ -n "${run_id:-}" ]]; then
echo "Cancelling child workflow ${workflow}: ${run_id}" >&2
gh run cancel "$run_id" >/dev/null 2>&1 || true
fi
}
trap cancel_child EXIT INT TERM
while true; do
status="$(gh run view "$run_id" --json status --jq '.status')"
if [[ "$status" == "completed" ]]; then
break
fi
sleep 30
done
trap - EXIT INT TERM
conclusion="$(gh run view "$run_id" --json conclusion --jq '.conclusion')"
url="$(gh run view "$run_id" --json url --jq '.url')"
echo "${workflow} finished with ${conclusion}: ${url}"
echo "url=${url}" >> "$GITHUB_OUTPUT"
echo "conclusion=${conclusion}" >> "$GITHUB_OUTPUT"
if [[ "$conclusion" != "success" ]]; then
gh run view "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' || true
fi
}
{
echo "### Plugin prerelease"
echo
echo "- Target ref: \`${TARGET_REF}\`"
echo "- Target SHA: \`${TARGET_SHA}\`"
} >> "$GITHUB_STEP_SUMMARY"
dispatch_and_wait plugin-prerelease.yml -f target_ref="$TARGET_SHA" -f expected_sha="$TARGET_SHA" -f full_release_validation=true
release_checks:
name: Run release/live/Docker/QA validation
needs: [resolve_target]
if: contains(fromJSON('["all","release-checks","install-smoke","cross-os","live-e2e","package","qa","qa-parity","qa-live"]'), inputs.rerun_group)
runs-on: ubuntu-24.04
timeout-minutes: 720
outputs:
run_id: ${{ steps.dispatch.outputs.run_id }}
url: ${{ steps.dispatch.outputs.url }}
conclusion: ${{ steps.dispatch.outputs.conclusion }}
steps:
- name: Dispatch and monitor release checks
id: dispatch
env:
GH_TOKEN: ${{ github.token }}
TARGET_REF: ${{ inputs.ref }}
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
PROVIDER: ${{ inputs.provider }}
MODE: ${{ inputs.mode }}
RELEASE_PROFILE: ${{ inputs.release_profile }}
RERUN_GROUP: ${{ inputs.rerun_group }}
LIVE_SUITE_FILTER: ${{ inputs.live_suite_filter }}
run: |
set -euo pipefail
dispatch_and_wait() {
local workflow="$1"
shift
local before_json dispatch_output run_id status conclusion url
before_json="$(gh run list --workflow "$workflow" --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
dispatch_output="$(gh workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1)"
printf '%s\n' "$dispatch_output"
run_id="$(
printf '%s\n' "$dispatch_output" |
sed -nE 's#.*actions/runs/([0-9]+).*#\1#p' |
tail -n 1
)"
if [[ -z "$run_id" ]]; then
for _ in $(seq 1 60); do
run_id="$(
BEFORE_IDS="$before_json" gh run list --workflow "$workflow" --event workflow_dispatch --limit 50 --json databaseId,createdAt \
--jq 'map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty'
)"
if [[ -n "$run_id" ]]; then
break
fi
sleep 5
done
fi
if [[ -z "${run_id:-}" ]]; then
echo "Could not find dispatched run for ${workflow}." >&2
exit 1
fi
echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"
cancel_child() {
if [[ -n "${run_id:-}" ]]; then
echo "Cancelling child workflow ${workflow}: ${run_id}" >&2
gh run cancel "$run_id" >/dev/null 2>&1 || true
fi
}
trap cancel_child EXIT INT TERM
while true; do
status="$(gh run view "$run_id" --json status --jq '.status')"
if [[ "$status" == "completed" ]]; then
break
fi
sleep 30
done
trap - EXIT INT TERM
conclusion="$(gh run view "$run_id" --json conclusion --jq '.conclusion')"
url="$(gh run view "$run_id" --json url --jq '.url')"
echo "${workflow} finished with ${conclusion}: ${url}"
echo "url=${url}" >> "$GITHUB_OUTPUT"
echo "conclusion=${conclusion}" >> "$GITHUB_OUTPUT"
if [[ "$conclusion" != "success" ]]; then
gh run view "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' || true
fi
}
{
echo "### Release/live/Docker/QA validation"
echo
echo "- Target ref: \`${TARGET_REF}\`"
echo "- Target SHA: \`${TARGET_SHA}\`"
echo "- Provider: \`${PROVIDER}\`"
echo "- Cross-OS mode: \`${MODE}\`"
echo "- Release profile: \`${RELEASE_PROFILE}\`"
echo "- Rerun group: \`${RERUN_GROUP}\`"
if [[ -n "${LIVE_SUITE_FILTER// }" ]]; then
echo "- Live suite filter: \`${LIVE_SUITE_FILTER}\`"
fi
} >> "$GITHUB_STEP_SUMMARY"
child_rerun_group="$RERUN_GROUP"
if [[ "$child_rerun_group" == "release-checks" ]]; then
child_rerun_group=all
fi
args=(
-f ref="$TARGET_SHA"
-f expected_sha="$TARGET_SHA"
-f provider="$PROVIDER"
-f mode="$MODE"
-f release_profile="$RELEASE_PROFILE"
-f rerun_group="$child_rerun_group"
)
if [[ -n "${LIVE_SUITE_FILTER// }" ]]; then
args+=(-f live_suite_filter="$LIVE_SUITE_FILTER")
fi
dispatch_and_wait openclaw-release-checks.yml "${args[@]}"
npm_telegram:
name: Run post-publish Telegram E2E
needs: [resolve_target]
if: inputs.npm_telegram_package_spec != '' && contains(fromJSON('["all","npm-telegram"]'), inputs.rerun_group)
runs-on: ubuntu-24.04
timeout-minutes: 120
outputs:
run_id: ${{ steps.dispatch.outputs.run_id }}
url: ${{ steps.dispatch.outputs.url }}
conclusion: ${{ steps.dispatch.outputs.conclusion }}
steps:
- name: Dispatch and monitor npm Telegram E2E
id: dispatch
env:
GH_TOKEN: ${{ github.token }}
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
PACKAGE_SPEC: ${{ inputs.npm_telegram_package_spec }}
PROVIDER_MODE: ${{ inputs.npm_telegram_provider_mode }}
SCENARIO: ${{ inputs.npm_telegram_scenario }}
run: |
set -euo pipefail
before_json="$(gh run list --workflow npm-telegram-beta-e2e.yml --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
args=(-f package_spec="$PACKAGE_SPEC" -f harness_ref="$TARGET_SHA" -f provider_mode="$PROVIDER_MODE")
if [[ -n "${SCENARIO// }" ]]; then
args+=(-f scenario="$SCENARIO")
fi
gh workflow run npm-telegram-beta-e2e.yml --ref "$CHILD_WORKFLOW_REF" "${args[@]}"
run_id=""
for _ in $(seq 1 60); do
run_id="$(
BEFORE_IDS="$before_json" gh run list --workflow npm-telegram-beta-e2e.yml --event workflow_dispatch --limit 50 --json databaseId,createdAt \
--jq 'map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty'
)"
if [[ -n "$run_id" ]]; then
break
fi
sleep 5
done
if [[ -z "$run_id" ]]; then
echo "Could not find dispatched run for npm-telegram-beta-e2e.yml." >&2
exit 1
fi
echo "Dispatched npm-telegram-beta-e2e.yml: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"
cancel_child() {
if [[ -n "${run_id:-}" ]]; then
echo "Cancelling child workflow npm-telegram-beta-e2e.yml: ${run_id}" >&2
gh run cancel "$run_id" >/dev/null 2>&1 || true
fi
}
trap cancel_child EXIT INT TERM
while true; do
status="$(gh run view "$run_id" --json status --jq '.status')"
if [[ "$status" == "completed" ]]; then
break
fi
sleep 30
done
trap - EXIT INT TERM
conclusion="$(gh run view "$run_id" --json conclusion --jq '.conclusion')"
url="$(gh run view "$run_id" --json url --jq '.url')"
echo "npm-telegram-beta-e2e.yml finished with ${conclusion}: ${url}"
echo "url=${url}" >> "$GITHUB_OUTPUT"
echo "conclusion=${conclusion}" >> "$GITHUB_OUTPUT"
if [[ "$conclusion" != "success" ]]; then
gh run view "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' || true
fi
summary:
name: Verify full validation
needs: [normal_ci, plugin_prerelease, release_checks, npm_telegram]
if: always()
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Request private evidence update
env:
RELEASE_PRIVATE_DISPATCH_TOKEN: ${{ secrets.OPENCLAW_RELEASES_PRIVATE_DISPATCH_TOKEN }}
TARGET_REF: ${{ inputs.ref }}
PACKAGE_SPEC: ${{ inputs.evidence_package_spec || inputs.npm_telegram_package_spec }}
GITHUB_RUN_ID_VALUE: ${{ github.run_id }}
RELEASE_CHECKS_RESULT: ${{ needs.release_checks.result }}
run: |
set -euo pipefail
if [[ "$RELEASE_CHECKS_RESULT" == "skipped" ]]; then
echo "Release checks were skipped by rerun group; skipping automatic private evidence update."
exit 0
fi
if [[ -z "${RELEASE_PRIVATE_DISPATCH_TOKEN// }" ]]; then
echo "OPENCLAW_RELEASES_PRIVATE_DISPATCH_TOKEN is not configured; skipping automatic private evidence update."
exit 0
fi
release_id="${TARGET_REF#refs/tags/}"
release_id="${release_id#v}"
if [[ "$PACKAGE_SPEC" =~ ^openclaw@(.+)$ ]]; then
release_id="${BASH_REMATCH[1]}"
fi
release_id="$(printf '%s' "$release_id" | tr '/:@ ' '----' | tr -cd 'A-Za-z0-9._-')"
if [[ -z "$release_id" ]]; then
echo "::error::Could not derive release evidence id from target ref '${TARGET_REF}'."
exit 1
fi
payload="$(
jq -cn \
--arg full_validation_run_id "$GITHUB_RUN_ID_VALUE" \
--arg release_id "$release_id" \
--arg release_ref "$TARGET_REF" \
--arg package_spec "$PACKAGE_SPEC" \
--arg notes "Automatically requested by Full Release Validation ${GITHUB_RUN_ID_VALUE} after child workflows completed; the parent summary re-checks current child run conclusions." \
'{
event_type: "openclaw_full_release_validation_completed",
client_payload: {
full_validation_run_id: $full_validation_run_id,
release_id: $release_id,
release_ref: $release_ref,
package_spec: $package_spec,
notes: $notes
}
}'
)"
curl --fail-with-body \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${RELEASE_PRIVATE_DISPATCH_TOKEN}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/openclaw/releases-private/dispatches \
-d "$payload"
- name: Verify child workflow results
env:
GH_TOKEN: ${{ github.token }}
NORMAL_CI_RUN_ID: ${{ needs.normal_ci.outputs.run_id }}
PLUGIN_PRERELEASE_RUN_ID: ${{ needs.plugin_prerelease.outputs.run_id }}
RELEASE_CHECKS_RUN_ID: ${{ needs.release_checks.outputs.run_id }}
NPM_TELEGRAM_RUN_ID: ${{ needs.npm_telegram.outputs.run_id }}
NORMAL_CI_RESULT: ${{ needs.normal_ci.result }}
PLUGIN_PRERELEASE_RESULT: ${{ needs.plugin_prerelease.result }}
RELEASE_CHECKS_RESULT: ${{ needs.release_checks.result }}
NPM_TELEGRAM_RESULT: ${{ needs.npm_telegram.result }}
run: |
set -euo pipefail
check_child() {
local label="$1"
local run_id="$2"
local required="$3"
if [[ -z "${run_id// }" ]]; then
if [[ "$required" == "0" ]]; then
echo "${label}: skipped"
return 0
fi
echo "::error::${label} did not record a child run id."
return 1
fi
local run_json status conclusion url attempt
run_json="$(gh run view "$run_id" --json status,conclusion,url,attempt,jobs)"
status="$(jq -r '.status' <<< "$run_json")"
conclusion="$(jq -r '.conclusion' <<< "$run_json")"
url="$(jq -r '.url' <<< "$run_json")"
attempt="$(jq -r '.attempt' <<< "$run_json")"
echo "${label}: ${status}/${conclusion} attempt ${attempt}: ${url}"
if [[ "$status" != "completed" || "$conclusion" != "success" ]]; then
echo "::error::${label} child run ended with ${status}/${conclusion}: ${url}"
jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, status, conclusion, url}' <<< "$run_json" || true
return 1
fi
}
append_child_overview() {
{
echo
echo "### Child workflow overview"
echo
echo "| Child | Result | Minutes | Run |"
echo "| --- | --- | ---: | --- |"
} >> "$GITHUB_STEP_SUMMARY"
append_child_row() {
local label="$1"
local run_id="$2"
local result="$3"
if [[ -z "${run_id// }" ]]; then
echo "| \`${label}\` | \`${result}\` | | skipped |" >> "$GITHUB_STEP_SUMMARY"
return 0
fi
local run_json row
run_json="$(gh run view "$run_id" --json status,conclusion,url,createdAt,updatedAt)"
row="$(
jq -r --arg label "$label" '
def ts: fromdateiso8601;
. as $run |
($run.createdAt // "") as $created |
($run.updatedAt // "") as $updated |
(if ($created | length) > 0 and ($updated | length) > 0
then (((($updated | ts) - ($created | ts)) / 60) * 10 | round / 10 | tostring)
else ""
end) as $minutes |
"| `" + $label + "` | `" + ($run.status // "") + "/" + ($run.conclusion // "") + "` | " + $minutes + " | [run](" + ($run.url // "") + ") |"
' <<< "$run_json"
)"
echo "$row" >> "$GITHUB_STEP_SUMMARY"
}
append_child_row "normal_ci" "$NORMAL_CI_RUN_ID" "$NORMAL_CI_RESULT"
append_child_row "plugin_prerelease" "$PLUGIN_PRERELEASE_RUN_ID" "$PLUGIN_PRERELEASE_RESULT"
append_child_row "release_checks" "$RELEASE_CHECKS_RUN_ID" "$RELEASE_CHECKS_RESULT"
append_child_row "npm_telegram" "$NPM_TELEGRAM_RUN_ID" "$NPM_TELEGRAM_RESULT"
}
summarize_child_timing() {
local label="$1"
local run_id="$2"
if [[ -z "${run_id// }" ]]; then
return 0
fi
{
echo
echo "### Slowest jobs: ${label}"
echo
gh run view "$run_id" --json jobs --jq '
def ts: fromdateiso8601;
"| Job | Result | Minutes |",
"| --- | --- | ---: |",
([.jobs[]
| select(.startedAt != "0001-01-01T00:00:00Z" and .completedAt != "0001-01-01T00:00:00Z")
| . + {durationMin: ((((.completedAt | ts) - (.startedAt | ts)) / 60) * 10 | round / 10)}
| {name, conclusion, durationMin}]
| sort_by(.durationMin)
| reverse
| .[0:10]
| map("| `" + (.name | gsub("\\|"; "\\|")) + "` | `" + ((.conclusion // "") | tostring) + "` | " + (.durationMin | tostring) + " |")
| .[])
' || echo "_Unable to summarize jobs for run ${run_id}._"
echo
echo "### Longest queues: ${label}"
echo
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/jobs?per_page=100" --jq ".jobs[] | @json" | jq -sr '
def ts: fromdateiso8601;
"| Job | Result | Queue minutes | Run minutes |",
"| --- | --- | ---: | ---: |",
([.[]
| select(.created_at != null and .started_at != null)
| . + {
queueMin: ((((.started_at | ts) - (.created_at | ts)) / 60) * 10 | round / 10),
durationMin: (if .completed_at == null then null else ((((.completed_at | ts) - (.started_at | ts)) / 60) * 10 | round / 10) end)
}
| select(.queueMin > 0)
| {name, conclusion, queueMin, durationMin}]
| sort_by(.queueMin)
| reverse
| .[0:10]
| map("| `" + (.name | gsub("\\|"; "\\|")) + "` | `" + ((.conclusion // "") | tostring) + "` | " + (.queueMin | tostring) + " | " + ((.durationMin // "") | tostring) + " |")
| .[])
' || echo "_Unable to summarize queue times for run ${run_id}._"
} >> "$GITHUB_STEP_SUMMARY"
}
failed=0
append_child_overview
if [[ "$NORMAL_CI_RESULT" == "skipped" && -z "${NORMAL_CI_RUN_ID// }" ]]; then
check_child "normal_ci" "" 0 || failed=1
else
check_child "normal_ci" "$NORMAL_CI_RUN_ID" 1 || failed=1
fi
if [[ "$PLUGIN_PRERELEASE_RESULT" == "skipped" && -z "${PLUGIN_PRERELEASE_RUN_ID// }" ]]; then
check_child "plugin_prerelease" "" 0 || failed=1
else
check_child "plugin_prerelease" "$PLUGIN_PRERELEASE_RUN_ID" 1 || failed=1
fi
if [[ "$RELEASE_CHECKS_RESULT" == "skipped" && -z "${RELEASE_CHECKS_RUN_ID// }" ]]; then
check_child "release_checks" "" 0 || failed=1
else
check_child "release_checks" "$RELEASE_CHECKS_RUN_ID" 1 || failed=1
fi
if [[ "$NPM_TELEGRAM_RESULT" == "skipped" && -z "${NPM_TELEGRAM_RUN_ID// }" ]]; then
check_child "npm_telegram" "" 0 || failed=1
else
check_child "npm_telegram" "$NPM_TELEGRAM_RUN_ID" 1 || failed=1
fi
summarize_child_timing "normal_ci" "$NORMAL_CI_RUN_ID"
summarize_child_timing "plugin_prerelease" "$PLUGIN_PRERELEASE_RUN_ID"
summarize_child_timing "release_checks" "$RELEASE_CHECKS_RUN_ID"
summarize_child_timing "npm_telegram" "$NPM_TELEGRAM_RUN_ID"
exit "$failed"

View File

@@ -34,11 +34,10 @@ on:
permissions:
contents: read
packages: write
concurrency:
group: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && format('{0}-{1}-{2}', github.workflow, github.event_name, github.run_id) || format('{0}-{1}', github.workflow, github.ref) }}
cancel-in-progress: ${{ github.event_name != 'workflow_call' }}
group: ${{ github.event_name == 'workflow_dispatch' && format('{0}-manual-{1}', github.workflow, github.run_id) || format('{0}-{1}', github.workflow, github.ref) }}
cancel-in-progress: true
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
@@ -52,8 +51,6 @@ jobs:
run_fast_install_smoke: ${{ steps.manifest.outputs.run_fast_install_smoke }}
run_full_install_smoke: ${{ steps.manifest.outputs.run_full_install_smoke }}
run_bun_global_install_smoke: ${{ steps.manifest.outputs.run_bun_global_install_smoke }}
target_sha: ${{ steps.manifest.outputs.target_sha }}
dockerfile_image: ${{ steps.manifest.outputs.dockerfile_image }}
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -77,9 +74,6 @@ jobs:
run_full_install_smoke=true
run_bun_global_install_smoke=false
run_install_smoke=true
target_sha="$(git rev-parse HEAD)"
owner="$(printf '%s' "${GITHUB_REPOSITORY_OWNER:-openclaw}" | tr '[:upper:]' '[:lower:]')"
dockerfile_image="ghcr.io/${owner}/openclaw-dockerfile-smoke:${target_sha}"
if [ "$event_name" = "schedule" ]; then
run_bun_global_install_smoke=true
elif [ "$event_name" = "workflow_dispatch" ] || [ "$event_name" = "workflow_call" ]; then
@@ -93,8 +87,6 @@ jobs:
echo "run_fast_install_smoke=$run_fast_install_smoke"
echo "run_full_install_smoke=$run_full_install_smoke"
echo "run_bun_global_install_smoke=$run_bun_global_install_smoke"
echo "target_sha=$target_sha"
echo "dockerfile_image=$dockerfile_image"
} >> "$GITHUB_OUTPUT"
install-smoke-fast:
@@ -111,23 +103,23 @@ jobs:
ref: ${{ inputs.ref || github.ref }}
- name: Set up Blacksmith Docker Builder
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1
with:
max-cache-size-mb: 800000
uses: useblacksmith/setup-docker-builder@ac083cc84672d01c60d5e8561d0a939b697de542 # v1
# Keep release smoke builds bounded and log-producing. The Blacksmith
# build action can leave jobs in-progress without step logs when a remote
# builder stalls; an explicit buildx invocation fails closed instead.
# Blacksmith's builder owns the Docker layer cache; keep smoke builds off
# explicit gha cache directives so local tags still load cleanly.
- name: Build root Dockerfile smoke image
run: |
timeout 45m docker buildx build \
--progress=plain \
--load \
--build-arg OPENCLAW_EXTENSIONS=matrix \
-t openclaw-dockerfile-smoke:local \
-t openclaw-ext-smoke:local \
-f ./Dockerfile \
.
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2
with:
context: .
file: ./Dockerfile
build-args: |
OPENCLAW_EXTENSIONS=matrix
tags: |
openclaw-dockerfile-smoke:local
openclaw-ext-smoke:local
load: true
push: false
provenance: false
- name: Run root Dockerfile CLI smoke
run: |
@@ -204,12 +196,10 @@ jobs:
"
'
root_dockerfile_image:
install-smoke:
needs: [preflight]
if: needs.preflight.outputs.run_full_install_smoke == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
outputs:
image_ref: ${{ steps.image.outputs.image_ref }}
env:
DOCKER_BUILD_SUMMARY: "false"
DOCKER_BUILD_RECORD_UPLOAD: "false"
@@ -219,127 +209,51 @@ jobs:
with:
ref: ${{ inputs.ref || github.ref }}
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Check for existing root Dockerfile smoke image
id: existing
env:
IMAGE_REF: ${{ needs.preflight.outputs.dockerfile_image }}
run: |
set -euo pipefail
if timeout 180s docker pull "$IMAGE_REF"; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "Using existing root Dockerfile smoke image: \`$IMAGE_REF\`" >> "$GITHUB_STEP_SUMMARY"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "No existing root Dockerfile smoke image found for \`$IMAGE_REF\`; building it." >> "$GITHUB_STEP_SUMMARY"
fi
- name: Set up Blacksmith Docker Builder
if: steps.existing.outputs.exists != 'true'
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1
with:
max-cache-size-mb: 800000
# Build once with the matrix extension and publish by target SHA. Use a
# direct buildx command so release jobs emit Docker progress and time out.
- name: Build and push root Dockerfile smoke image
if: steps.existing.outputs.exists != 'true'
env:
IMAGE_REF: ${{ needs.preflight.outputs.dockerfile_image }}
run: |
timeout 45m docker buildx build \
--progress=plain \
--push \
--build-arg OPENCLAW_EXTENSIONS=matrix \
-t "$IMAGE_REF" \
-f ./Dockerfile \
.
- name: Record root image output
id: image
env:
IMAGE_REF: ${{ needs.preflight.outputs.dockerfile_image }}
run: echo "image_ref=$IMAGE_REF" >> "$GITHUB_OUTPUT"
- name: Summarize root image
env:
IMAGE_REF: ${{ needs.preflight.outputs.dockerfile_image }}
TARGET_SHA: ${{ needs.preflight.outputs.target_sha }}
run: |
{
echo "## Root Dockerfile smoke image"
echo
echo "- Target SHA: \`${TARGET_SHA}\`"
echo "- Image: \`${IMAGE_REF}\`"
echo "- Reused existing image: \`${{ steps.existing.outputs.exists }}\`"
} >> "$GITHUB_STEP_SUMMARY"
qr_package_install_smoke:
needs: [preflight]
if: needs.preflight.outputs.run_full_install_smoke == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
- name: Checkout CLI
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref }}
uses: useblacksmith/setup-docker-builder@ac083cc84672d01c60d5e8561d0a939b697de542 # v1
# Blacksmith's builder owns the Docker layer cache; keep smoke builds off
# explicit gha cache directives so local tags still load cleanly.
- name: Run QR package install smoke
env:
OPENCLAW_QR_SMOKE_FORCE_INSTALL: "1"
run: bash scripts/e2e/qr-import-docker.sh
root_dockerfile_smokes:
needs: [preflight, root_dockerfile_image]
if: needs.preflight.outputs.run_full_install_smoke == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
- name: Checkout CLI
uses: actions/checkout@v6
# Build once with the matrix extension and tag both smoke names. This
# keeps the build-arg coverage without a second Blacksmith build action.
- name: Build root Dockerfile smoke image
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2
with:
ref: ${{ inputs.ref || github.ref }}
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Pull root Dockerfile smoke image
env:
IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }}
run: timeout 300s docker pull "$IMAGE_REF"
context: .
file: ./Dockerfile
build-args: |
OPENCLAW_EXTENSIONS=matrix
tags: |
openclaw-dockerfile-smoke:local
openclaw-ext-smoke:local
load: true
push: false
provenance: false
- name: Run root Dockerfile CLI smoke
env:
IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }}
run: |
docker run --rm --entrypoint sh "$IMAGE_REF" -lc 'which openclaw && openclaw --version'
docker run --rm --entrypoint sh openclaw-dockerfile-smoke:local -lc 'which openclaw && openclaw --version'
- name: Run agents delete shared workspace Docker CLI smoke
env:
OPENCLAW_AGENTS_DELETE_SHARED_WORKSPACE_E2E_IMAGE: ${{ needs.root_dockerfile_image.outputs.image_ref }}
OPENCLAW_AGENTS_DELETE_SHARED_WORKSPACE_E2E_IMAGE: openclaw-dockerfile-smoke:local
OPENCLAW_AGENTS_DELETE_SHARED_WORKSPACE_E2E_SKIP_BUILD: "1"
run: bash scripts/e2e/agents-delete-shared-workspace-docker.sh
- name: Run Docker gateway network e2e
env:
OPENCLAW_GATEWAY_NETWORK_E2E_IMAGE: ${{ needs.root_dockerfile_image.outputs.image_ref }}
OPENCLAW_GATEWAY_NETWORK_E2E_IMAGE: openclaw-dockerfile-smoke:local
OPENCLAW_GATEWAY_NETWORK_E2E_SKIP_BUILD: "1"
run: bash scripts/e2e/gateway-network-docker.sh
- name: Smoke test Dockerfile with matrix extension build arg
env:
IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }}
run: |
docker run --rm --entrypoint sh "$IMAGE_REF" -lc '
docker run --rm --entrypoint sh openclaw-ext-smoke:local -lc '
which openclaw &&
openclaw --version &&
node -e "
@@ -382,60 +296,39 @@ jobs:
"
'
installer_smoke:
needs: [preflight, root_dockerfile_image]
if: needs.preflight.outputs.run_full_install_smoke == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
env:
DOCKER_BUILD_SUMMARY: "false"
DOCKER_BUILD_RECORD_UPLOAD: "false"
steps:
- name: Checkout CLI
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref }}
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Pull root Dockerfile smoke image
env:
IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }}
run: timeout 300s docker pull "$IMAGE_REF"
- name: Set up Blacksmith Docker Builder
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1
with:
max-cache-size-mb: 800000
- name: Build installer smoke image
run: |
timeout 20m docker buildx build \
--progress=plain \
--load \
-t openclaw-install-smoke:local \
-f ./scripts/docker/install-sh-smoke/Dockerfile \
./scripts/docker
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2
with:
context: ./scripts/docker
file: ./scripts/docker/install-sh-smoke/Dockerfile
tags: openclaw-install-smoke:local
load: true
push: false
provenance: false
- name: Build installer non-root image
run: |
timeout 20m docker buildx build \
--progress=plain \
--load \
-t openclaw-install-nonroot:local \
-f ./scripts/docker/install-sh-nonroot/Dockerfile \
./scripts/docker
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2
with:
context: ./scripts/docker
file: ./scripts/docker/install-sh-nonroot/Dockerfile
tags: openclaw-install-nonroot:local
load: true
push: false
provenance: false
- name: Setup Node environment for installer smoke
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
install-bun: ${{ needs.preflight.outputs.run_bun_global_install_smoke }}
install-deps: "true"
- name: Run Bun global install image-provider smoke
if: needs.preflight.outputs.run_bun_global_install_smoke == 'true'
env:
OPENCLAW_BUN_GLOBAL_SMOKE_DIST_IMAGE: openclaw-dockerfile-smoke:local
OPENCLAW_BUN_GLOBAL_SMOKE_HOST_BUILD: "0"
run: bash scripts/e2e/bun-global-install-smoke.sh
- name: Run installer docker tests
env:
OPENCLAW_INSTALL_URL: https://openclaw.ai/install.sh
@@ -448,49 +341,15 @@ jobs:
OPENCLAW_INSTALL_SMOKE_SKIP_NPM_GLOBAL: "1"
OPENCLAW_INSTALL_SMOKE_SKIP_PREVIOUS: "1"
OPENCLAW_INSTALL_SMOKE_UPDATE_BASELINE: ${{ inputs.update_baseline_version || 'latest' }}
OPENCLAW_INSTALL_SMOKE_UPDATE_DIST_IMAGE: ${{ needs.root_dockerfile_image.outputs.image_ref }}
OPENCLAW_INSTALL_SMOKE_UPDATE_DIST_IMAGE: openclaw-dockerfile-smoke:local
OPENCLAW_INSTALL_SMOKE_UPDATE_SKIP_LOCAL_BUILD: "1"
run: bash scripts/test-install-sh-docker.sh
bun_global_install_smoke:
needs: [preflight, root_dockerfile_image]
if: needs.preflight.outputs.run_full_install_smoke == 'true' && needs.preflight.outputs.run_bun_global_install_smoke == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
- name: Checkout CLI
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref }}
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Pull root Dockerfile smoke image
env:
IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }}
run: timeout 300s docker pull "$IMAGE_REF"
- name: Setup Node environment for Bun smoke
uses: ./.github/actions/setup-node-env
with:
install-bun: "true"
install-deps: "true"
- name: Run Bun global install image-provider smoke
env:
OPENCLAW_BUN_GLOBAL_SMOKE_DIST_IMAGE: ${{ needs.root_dockerfile_image.outputs.image_ref }}
OPENCLAW_BUN_GLOBAL_SMOKE_HOST_BUILD: "0"
run: bash scripts/e2e/bun-global-install-smoke.sh
docker-e2e-fast:
needs: [preflight]
if: needs.preflight.outputs.run_fast_install_smoke == 'true' || needs.preflight.outputs.run_full_install_smoke == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 12
timeout-minutes: 8
env:
DOCKER_BUILD_SUMMARY: "false"
DOCKER_BUILD_RECORD_UPLOAD: "false"
@@ -501,9 +360,7 @@ jobs:
ref: ${{ inputs.ref || github.ref }}
- name: Set up Blacksmith Docker Builder
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1
with:
max-cache-size-mb: 800000
uses: useblacksmith/setup-docker-builder@ac083cc84672d01c60d5e8561d0a939b697de542 # v1
- name: Setup Node environment for package smoke
uses: ./.github/actions/setup-node-env
@@ -515,4 +372,4 @@ jobs:
env:
OPENCLAW_BUNDLED_CHANNEL_DEPS_E2E_IMAGE: openclaw-bundled-channel-fast:local
OPENCLAW_BUNDLED_CHANNEL_DOCKER_RUN_TIMEOUT: 90s
run: timeout 480s pnpm test:docker:bundled-channel-deps:fast
run: timeout 240s pnpm test:docker:bundled-channel-deps:fast

View File

@@ -278,7 +278,6 @@ jobs:
const labelColor = "B60205";
const labelDescription = `Author has more than ${activePrLimit} active PRs in this repo`;
const authorLogin = pullRequest.user?.login;
const headRefName = pullRequest.head?.ref ?? "";
if (!authorLogin) {
return;
}
@@ -296,25 +295,6 @@ jobs:
.filter((name) => typeof name === "string"),
);
if (pullRequest.user?.type === "Bot" || /\[bot\]$/i.test(authorLogin) || authorLogin.startsWith("app/")) {
if (labelNames.has(activePrLimitLabel)) {
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
name: activePrLimitLabel,
});
} catch (error) {
if (error?.status !== 404) {
throw error;
}
}
}
core.info(`Skipping active PR limit for GitHub App author ${authorLogin}.`);
return;
}
if (labelNames.has(activePrLimitOverrideLabel)) {
if (labelNames.has(activePrLimitLabel)) {
try {
@@ -394,12 +374,7 @@ jobs:
return false;
};
const automationPrHeadPrefixes = ["clawsweeper/", "clownfish/"];
const isAutomationPullRequest =
typeof headRefName === "string" &&
automationPrHeadPrefixes.some((prefix) => headRefName.startsWith(prefix));
if ((await isPrivilegedAuthor()) || isAutomationPullRequest) {
if (await isPrivilegedAuthor()) {
if (labelNames.has(activePrLimitLabel)) {
try {
await github.rest.issues.removeLabel({

View File

@@ -1,54 +0,0 @@
name: Live Media Runner Image
on:
workflow_dispatch:
push:
branches: [main]
paths:
- ".github/images/live-media-runner/Dockerfile"
- ".github/workflows/live-media-runner-image.yml"
permissions:
contents: read
packages: write
concurrency:
group: live-media-runner-image-${{ github.ref }}
cancel-in-progress: true
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
build:
name: Build live media runner image
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Login to GHCR
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Set up Blacksmith Docker Builder
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1
with:
max-cache-size-mb: 800000
- name: Build and push live media runner image
uses: useblacksmith/build-push-action@fb9e3e6a9299c78462bfadd0d93352c316adc9b8 # v2
with:
context: .github/images/live-media-runner
file: .github/images/live-media-runner/Dockerfile
platforms: linux/amd64
tags: |
ghcr.io/openclaw/openclaw-live-media-runner:ubuntu-24.04
ghcr.io/openclaw/openclaw-live-media-runner:${{ github.sha }}
sbom: true
provenance: mode=max
push: true

View File

@@ -12,11 +12,6 @@ on:
required: true
default: true
type: boolean
public_release_branch:
description: Public branch that contains the release tag commit, usually main or release/YYYY.M.D
required: false
default: main
type: string
concurrency:
group: macos-release-${{ inputs.tag }}
@@ -71,17 +66,13 @@ jobs:
- name: Validate release tag and package metadata
env:
RELEASE_TAG: ${{ inputs.tag }}
PUBLIC_RELEASE_BRANCH: ${{ inputs.public_release_branch }}
WORKFLOW_REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
if [[ "${PUBLIC_RELEASE_BRANCH}" != "main" && ! "${PUBLIC_RELEASE_BRANCH}" =~ ^release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$ ]]; then
echo "public_release_branch must be main or release/YYYY.M.D, got ${PUBLIC_RELEASE_BRANCH}." >&2
exit 1
fi
RELEASE_SHA=$(git rev-parse HEAD)
RELEASE_MAIN_REF="refs/remotes/origin/${PUBLIC_RELEASE_BRANCH}"
RELEASE_MAIN_REF="refs/remotes/origin/${WORKFLOW_REF_NAME}"
export RELEASE_SHA RELEASE_TAG RELEASE_MAIN_REF
git fetch --no-tags origin "+refs/heads/${PUBLIC_RELEASE_BRANCH}:refs/remotes/origin/${PUBLIC_RELEASE_BRANCH}"
git fetch --no-tags origin "+refs/heads/${WORKFLOW_REF_NAME}:refs/remotes/origin/${WORKFLOW_REF_NAME}"
pnpm release:openclaw:npm:check
- name: Summarize next step

View File

@@ -1,99 +0,0 @@
name: Maintainer Command Reactions
on:
issue_comment:
types: [created, edited]
permissions: {}
concurrency:
group: maintainer-command-reactions-${{ github.event.comment.id }}
cancel-in-progress: true
jobs:
react:
if: ${{ !endsWith(github.actor, '[bot]') }}
runs-on: ubuntu-24.04
permissions:
issues: write
pull-requests: write
env:
MAINTAINER_COMMAND_REACTIONS: ${{ vars.MAINTAINER_COMMAND_REACTIONS || '/autoclose,/clawsweeper autoclose,/clawsweeper automerge,/merge,/land,/landpr' }}
steps:
- name: React to maintainer slash command
uses: actions/github-script@v9
with:
script: |
const comment = context.payload.comment;
const issue = context.payload.issue;
const commands = (process.env.MAINTAINER_COMMAND_REACTIONS || "")
.split(",")
.map((command) => command.trim())
.filter(Boolean);
const commandLine = String(comment.body || "")
.split(/\r?\n/)
.map((line) => line.trim())
.find((line) => commands.some((command) => line === command || line.startsWith(`${command} `)));
if (!commandLine) {
core.info(`Skipping comment ${comment.id}; no tracked maintainer command found.`);
return;
}
const isAutocloseCommand =
commandLine === "/autoclose" ||
commandLine.startsWith("/autoclose ") ||
commandLine === "/clawsweeper autoclose" ||
commandLine.startsWith("/clawsweeper autoclose ");
if (!issue.pull_request && !isAutocloseCommand) {
core.info("Skipping non-autoclose command reaction because the comment is not on a pull request.");
return;
}
const maintainerPermissions = new Set(["admin", "maintain", "write"]);
let permission = "none";
try {
const result = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: comment.user.login,
});
permission = String(result.data.permission || "none").toLowerCase();
} catch (error) {
if (error.status !== 404) {
core.info(`Could not resolve repository permission for ${comment.user.login}: ${error.message}`);
}
}
if (!maintainerPermissions.has(permission)) {
core.info(
`Skipping non-maintainer command reaction for ${comment.user.login}; repository permission is ${permission}.`,
);
return;
}
async function react(content) {
try {
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: comment.id,
content,
});
core.info(`Added ${content} reaction to comment ${comment.id}.`);
} catch (error) {
if (error.status === 422 && /already exists/i.test(String(error.message))) {
core.info(`${content} reaction already exists on comment ${comment.id}.`);
return;
}
if (error.status === 403 && /resource not accessible by integration/i.test(String(error.message))) {
core.warning(`${content} reaction could not be added with this token: ${error.message}`);
return;
}
throw error;
}
}
await react("eyes");
core.info(`Maintainer command observed on ${issue.pull_request ? "PR" : "issue"} #${issue.number}: ${commandLine}`);

View File

@@ -4,25 +4,10 @@ on:
workflow_dispatch:
inputs:
package_spec:
description: Published OpenClaw package spec to test when no artifact is supplied
description: Published OpenClaw package spec to test
required: true
default: openclaw@beta
type: string
package_label:
description: Optional display label for an artifact-backed package candidate
required: false
default: ""
type: string
package_artifact_name:
description: Advanced package-under-test artifact name; leave blank for registry install
required: false
default: ""
type: string
harness_ref:
description: Source ref for the private QA harness; defaults to the dispatched workflow ref
required: false
default: ""
type: string
provider_mode:
description: QA provider mode
required: true
@@ -35,44 +20,6 @@ on:
description: Optional comma-separated Telegram scenario ids
required: false
type: string
workflow_call:
inputs:
package_spec:
description: Published OpenClaw package spec to test when no artifact is supplied
required: true
type: string
package_artifact_name:
description: Optional package-under-test artifact from the current workflow run
required: false
default: ""
type: string
package_label:
description: Optional display label for an artifact-backed package candidate
required: false
default: ""
type: string
harness_ref:
description: Source ref for the private QA harness; defaults to the called workflow ref
required: false
default: ""
type: string
provider_mode:
description: QA provider mode
required: false
default: mock-openai
type: string
scenario:
description: Optional comma-separated Telegram scenario ids
required: false
default: ""
type: string
secrets:
OPENAI_API_KEY:
required: false
OPENCLAW_QA_CONVEX_SITE_URL:
required: false
OPENCLAW_QA_CONVEX_SECRET_CI:
required: false
permissions:
contents: read
@@ -87,8 +34,8 @@ env:
PNPM_VERSION: "10.33.0"
jobs:
run_package_telegram_e2e:
name: Run package Telegram E2E
run_npm_telegram_beta_e2e:
name: Run published npm Telegram E2E
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 60
environment: qa-live-shared
@@ -101,16 +48,16 @@ jobs:
- name: Checkout dispatch ref
uses: actions/checkout@v6
with:
ref: ${{ inputs.harness_ref || github.sha }}
ref: ${{ github.sha }}
fetch-depth: 1
- name: Set up Blacksmith Docker Builder
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1
uses: useblacksmith/setup-docker-builder@ac083cc84672d01c60d5e8561d0a939b697de542 # v1
with:
max-cache-size-mb: 800000
- name: Build Docker E2E image
uses: useblacksmith/build-push-action@fb9e3e6a9299c78462bfadd0d93352c316adc9b8 # v2
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2
with:
context: .
file: ./scripts/e2e/Dockerfile
@@ -131,7 +78,6 @@ jobs:
- name: Validate inputs and secrets
env:
PACKAGE_SPEC: ${{ inputs.package_spec }}
PACKAGE_ARTIFACT_NAME: ${{ inputs.package_artifact_name || '' }}
PROVIDER_MODE: ${{ inputs.provider_mode }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
@@ -140,19 +86,10 @@ jobs:
run: |
set -euo pipefail
if [[ -z "${PACKAGE_ARTIFACT_NAME// }" ]]; then
if [[ ! "${PACKAGE_SPEC}" =~ ^openclaw@(beta|latest|[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*|-beta\.[1-9][0-9]*)?)$ ]]; then
echo "package_spec must be openclaw@beta, openclaw@latest, or an exact OpenClaw release version; got: ${PACKAGE_SPEC}" >&2
exit 1
fi
if [[ ! "${PACKAGE_SPEC}" =~ ^openclaw@(beta|latest|[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*|-beta\.[1-9][0-9]*)?)$ ]]; then
echo "package_spec must be openclaw@beta, openclaw@latest, or an exact OpenClaw release version; got: ${PACKAGE_SPEC}" >&2
exit 1
fi
case "${PROVIDER_MODE}" in
mock-openai | live-frontier) ;;
*)
echo "provider_mode must be mock-openai or live-frontier; got: ${PROVIDER_MODE}" >&2
exit 1
;;
esac
require_var() {
local key="$1"
@@ -168,14 +105,7 @@ jobs:
require_var OPENAI_API_KEY
fi
- name: Download package-under-test artifact
if: inputs.package_artifact_name != ''
uses: actions/download-artifact@v8
with:
name: ${{ inputs.package_artifact_name }}
path: .artifacts/telegram-package-under-test
- name: Run package Telegram E2E
- name: Run npm Telegram beta E2E
id: run_lane
shell: bash
env:
@@ -183,7 +113,6 @@ jobs:
OPENCLAW_SKIP_DOCKER_BUILD: "1"
OPENCLAW_DOCKER_E2E_IMAGE: openclaw-docker-e2e:local
OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC: ${{ inputs.package_spec }}
OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL: ${{ inputs.package_label }}
OPENCLAW_NPM_TELEGRAM_PROVIDER_MODE: ${{ inputs.provider_mode }}
OPENCLAW_NPM_TELEGRAM_CREDENTIAL_SOURCE: convex
OPENCLAW_NPM_TELEGRAM_CREDENTIAL_ROLE: ci
@@ -192,7 +121,6 @@ jobs:
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT: "1"
INPUT_SCENARIO: ${{ inputs.scenario }}
PACKAGE_ARTIFACT_NAME: ${{ inputs.package_artifact_name || '' }}
run: |
set -euo pipefail
@@ -200,20 +128,6 @@ jobs:
echo "output_dir=${output_dir}" >> "$GITHUB_OUTPUT"
export OPENCLAW_NPM_TELEGRAM_OUTPUT_DIR="${output_dir}"
if [[ -n "${PACKAGE_ARTIFACT_NAME// }" ]]; then
mapfile -t package_tgzs < <(find .artifacts/telegram-package-under-test -type f -name "*.tgz" | sort)
if [[ "${#package_tgzs[@]}" -ne 1 ]]; then
echo "package artifact ${PACKAGE_ARTIFACT_NAME} must contain exactly one .tgz; found ${#package_tgzs[@]}" >&2
exit 1
fi
export OPENCLAW_NPM_TELEGRAM_PACKAGE_TGZ="${package_tgzs[0]}"
if [[ -z "${OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL// }" ]]; then
export OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL="$(basename "${package_tgzs[0]}")"
fi
elif [[ -z "${OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL// }" ]]; then
export OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL="${OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC}"
fi
if [[ -n "${INPUT_SCENARIO// }" ]]; then
export OPENCLAW_NPM_TELEGRAM_SCENARIOS="${INPUT_SCENARIO}"
fi
@@ -225,6 +139,6 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: npm-telegram-beta-e2e-${{ github.run_id }}-${{ github.run_attempt }}
path: .artifacts/qa-e2e/
path: ${{ steps.run_lane.outputs.output_dir }}
retention-days: 14
if-no-files-found: warn

View File

@@ -51,31 +51,6 @@ on:
required: false
default: ""
type: string
candidate_artifact_name:
description: Optional current-run artifact name containing the candidate OpenClaw tarball
required: false
default: ""
type: string
candidate_artifact_run_id:
description: Optional workflow run id for candidate_artifact_name
required: false
default: ""
type: string
candidate_file_name:
description: Optional candidate tarball file name inside candidate_artifact_name
required: false
default: ""
type: string
candidate_version:
description: Optional candidate OpenClaw package version
required: false
default: ""
type: string
candidate_source_sha:
description: Optional source SHA used to build the candidate tarball
required: false
default: ""
type: string
workflow_call:
inputs:
ref:
@@ -115,31 +90,6 @@ on:
required: false
default: ""
type: string
candidate_artifact_name:
description: Optional current-run artifact name containing the candidate OpenClaw tarball
required: false
default: ""
type: string
candidate_artifact_run_id:
description: Optional workflow run id for candidate_artifact_name
required: false
default: ""
type: string
candidate_file_name:
description: Optional candidate tarball file name inside candidate_artifact_name
required: false
default: ""
type: string
candidate_version:
description: Optional candidate OpenClaw package version
required: false
default: ""
type: string
candidate_source_sha:
description: Optional source SHA used to build the candidate tarball
required: false
default: ""
type: string
secrets:
OPENAI_API_KEY:
required: false
@@ -158,7 +108,7 @@ permissions: read-all
concurrency:
group: openclaw-cross-os-release-checks-${{ inputs.ref }}-${{ inputs.provider }}-${{ inputs.mode }}
cancel-in-progress: ${{ inputs.ref == 'main' }}
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
@@ -166,11 +116,10 @@ env:
PNPM_VERSION: "10.32.1"
OPENCLAW_REPOSITORY: openclaw/openclaw
TSX_VERSION: "4.21.0"
OPENCLAW_CROSS_OS_OPENAI_MODEL: ${{ vars.OPENCLAW_CROSS_OS_OPENAI_MODEL || 'openai/gpt-5.4-mini' }}
jobs:
prepare:
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
outputs:
baseline_file_name: ${{ steps.baseline_metadata.outputs.file_name }}
baseline_spec: ${{ steps.baseline.outputs.value }}
@@ -311,7 +260,6 @@ jobs:
persist-credentials: false
- name: Checkout public source ref
if: inputs.candidate_artifact_name == ''
uses: actions/checkout@v6
with:
repository: ${{ env.OPENCLAW_REPOSITORY }}
@@ -322,7 +270,7 @@ jobs:
submodules: recursive
- name: Setup pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
run_install: false
@@ -332,74 +280,17 @@ jobs:
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
cache-dependency-path: ${{ inputs.candidate_artifact_name == '' && 'source/pnpm-lock.yaml' || 'workflow/pnpm-lock.yaml' }}
- name: Ensure pnpm store cache directory exists
run: mkdir -p "$(pnpm store path --silent)"
cache-dependency-path: source/pnpm-lock.yaml
- name: Build candidate artifact once
if: inputs.candidate_artifact_name == ''
env:
OUTPUT_DIR: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare
run: |
bash workflow/scripts/github/run-openclaw-cross-os-release-checks.sh \
pnpm dlx "tsx@${TSX_VERSION}" workflow/scripts/openclaw-cross-os-release-checks.ts \
--prepare-only \
--source-dir source \
--output-dir "${OUTPUT_DIR}"
- name: Download current-run candidate artifact
if: inputs.candidate_artifact_name != '' && inputs.candidate_artifact_run_id == ''
uses: actions/download-artifact@v8
with:
name: ${{ inputs.candidate_artifact_name }}
path: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare/package
- name: Download previous-run candidate artifact
if: inputs.candidate_artifact_name != '' && inputs.candidate_artifact_run_id != ''
uses: actions/download-artifact@v8
with:
name: ${{ inputs.candidate_artifact_name }}
run-id: ${{ inputs.candidate_artifact_run_id }}
github-token: ${{ github.token }}
path: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare/package
- name: Capture provided candidate artifact metadata
if: inputs.candidate_artifact_name != ''
env:
PACKAGE_DIR: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare/package
INPUT_CANDIDATE_FILE_NAME: ${{ inputs.candidate_file_name }}
INPUT_CANDIDATE_VERSION: ${{ inputs.candidate_version }}
INPUT_CANDIDATE_SOURCE_SHA: ${{ inputs.candidate_source_sha }}
CANDIDATE_JSON: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare/candidate.json
run: |
node <<'NODE'
const fs = require("node:fs");
const path = require("node:path");
const packageDir = process.env.PACKAGE_DIR;
const requestedFileName = process.env.INPUT_CANDIDATE_FILE_NAME.trim();
const files = fs.readdirSync(packageDir).filter((file) => file.endsWith(".tgz"));
const candidateFileName = requestedFileName || (files.length === 1 ? files[0] : "");
if (!candidateFileName) {
throw new Error(`Expected exactly one candidate .tgz in ${packageDir}; found ${files.length}.`);
}
if (!fs.existsSync(path.join(packageDir, candidateFileName))) {
throw new Error(`Provided candidate artifact does not contain ${candidateFileName}.`);
}
const candidateVersion = process.env.INPUT_CANDIDATE_VERSION.trim();
if (!candidateVersion) {
throw new Error("candidate_version is required when candidate_artifact_name is provided.");
}
const sourceSha = process.env.INPUT_CANDIDATE_SOURCE_SHA.trim();
if (!/^[0-9a-f]{40}$/iu.test(sourceSha)) {
throw new Error("candidate_source_sha must be a full commit SHA when candidate_artifact_name is provided.");
}
fs.writeFileSync(
process.env.CANDIDATE_JSON,
`${JSON.stringify({ candidateFileName, candidateVersion, sourceSha }, null, 2)}\n`,
);
NODE
- name: Resolve baseline package spec
if: ${{ inputs.mode != 'fresh' }}
id: baseline
@@ -479,7 +370,7 @@ jobs:
VAR_WINDOWS_RUNNER: ${{ vars.OPENCLAW_RELEASE_CHECKS_WINDOWS_RUNNER }}
VAR_MACOS_RUNNER: ${{ vars.OPENCLAW_RELEASE_CHECKS_MACOS_RUNNER }}
run: |
MATRIX_JSON="$(bash workflow/scripts/github/run-openclaw-cross-os-release-checks.sh \
MATRIX_JSON="$(pnpm dlx "tsx@${TSX_VERSION}" workflow/scripts/openclaw-cross-os-release-checks.ts \
--resolve-matrix \
--ref "${INPUT_REF}" \
--mode "${INPUT_MODE}" \
@@ -507,7 +398,7 @@ jobs:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
run_install: false
@@ -557,7 +448,7 @@ jobs:
if [[ -n "${OPENCLAW_DISCORD_SMOKE_BOT_TOKEN}" ]] && [[ -n "${OPENCLAW_DISCORD_SMOKE_GUILD_ID}" ]] && [[ -n "${OPENCLAW_DISCORD_SMOKE_CHANNEL_ID}" ]]; then
DISCORD_ARGS+=(--run-discord-roundtrip true)
fi
bash workflow/scripts/github/run-openclaw-cross-os-release-checks.sh \
pnpm dlx "tsx@${TSX_VERSION}" workflow/scripts/openclaw-cross-os-release-checks.ts \
--candidate-tgz "${CANDIDATE_TGZ}" \
--candidate-version "${CANDIDATE_VERSION}" \
--source-sha "${SOURCE_SHA}" \

File diff suppressed because it is too large Load Diff

View File

@@ -4,14 +4,9 @@ on:
workflow_dispatch:
inputs:
ref:
description: Branch, tag, or full commit SHA to validate
description: Existing release tag or current full 40-character workflow-branch commit SHA to validate (for example v2026.4.12 or 0123456789abcdef0123456789abcdef01234567)
required: true
type: string
expected_sha:
description: Optional full SHA that ref must resolve to
required: false
default: ""
type: string
provider:
description: Provider lane for cross-OS onboarding and the end-to-end agent turn
required: false
@@ -30,59 +25,28 @@ on:
- fresh
- upgrade
- both
release_profile:
description: Release coverage profile for live/Docker/provider breadth
required: false
default: stable
type: choice
options:
- minimum
- stable
- full
rerun_group:
description: Release check group to run
required: false
default: all
type: choice
options:
- all
- install-smoke
- cross-os
- live-e2e
- package
- qa
- qa-parity
- qa-live
live_suite_filter:
description: Optional exact live suite id for focused live/E2E reruns; blank runs all selected live suites
required: false
default: ""
type: string
concurrency:
group: openclaw-release-checks-${{ inputs.expected_sha || inputs.ref }}-${{ inputs.rerun_group }}
group: openclaw-release-checks-${{ inputs.ref }}
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "10.33.0"
OPENCLAW_CI_OPENAI_MODEL: ${{ vars.OPENCLAW_CI_OPENAI_MODEL || 'openai/gpt-5.5' }}
OPENCLAW_CI_OPENAI_MODEL: ${{ vars.OPENCLAW_CI_OPENAI_MODEL }}
jobs:
resolve_target:
runs-on: ubuntu-24.04
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 30
permissions:
contents: read
outputs:
ref: ${{ steps.inputs.outputs.ref }}
revision: ${{ steps.ref.outputs.sha }}
sha: ${{ steps.ref.outputs.sha }}
provider: ${{ steps.inputs.outputs.provider }}
mode: ${{ steps.inputs.outputs.mode }}
release_profile: ${{ steps.inputs.outputs.release_profile }}
rerun_group: ${{ steps.inputs.outputs.rerun_group }}
live_suite_filter: ${{ steps.inputs.outputs.live_suite_filter }}
steps:
- name: Require main or release workflow ref for release checks
env:
@@ -97,240 +61,89 @@ jobs:
- name: Validate ref input
env:
RELEASE_REF: ${{ inputs.ref }}
EXPECTED_SHA: ${{ inputs.expected_sha }}
run: |
set -euo pipefail
if [[ -z "${RELEASE_REF// }" ]] || [[ "${RELEASE_REF}" == -* ]]; then
echo "Expected a branch, tag, or full commit SHA; got: ${RELEASE_REF}" >&2
exit 1
fi
if [[ -n "${EXPECTED_SHA// }" ]] && [[ ! "${EXPECTED_SHA}" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "Expected expected_sha to be a full commit SHA; got: ${EXPECTED_SHA}" >&2
if [[ ! "${RELEASE_REF}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*((-beta\.[1-9][0-9]*)|(-[1-9][0-9]*))?$ ]] && [[ ! "${RELEASE_REF}" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "Expected an existing release tag or current full 40-character workflow-branch commit SHA, got: ${RELEASE_REF}" >&2
exit 1
fi
- name: Checkout trusted workflow helper
- name: Checkout selected ref
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ github.ref_name }}
path: workflow
fetch-depth: 1
- name: Fast-resolve selected ref
id: fast_ref
env:
RELEASE_REF: ${{ inputs.ref }}
EXPECTED_SHA: ${{ inputs.expected_sha }}
run: |
bash workflow/scripts/github/resolve-openclaw-ref.sh \
--ref "$RELEASE_REF" \
--expected-sha "$EXPECTED_SHA" \
--fallback-ok \
--github-output "$GITHUB_OUTPUT"
- name: Checkout selected ref for reachability fallback
if: steps.fast_ref.outputs.fallback == 'true'
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ inputs.ref }}
path: source
fetch-depth: 0
- name: Resolve checked-out fallback SHA
if: steps.fast_ref.outputs.fallback == 'true'
id: fallback_ref
working-directory: source
- name: Resolve checked-out SHA
id: ref
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Validate selected ref belongs to this repository
if: steps.fast_ref.outputs.fallback == 'true'
working-directory: source
- name: Validate selected ref is on workflow branch
env:
RELEASE_REF: ${{ inputs.ref }}
WORKFLOW_REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
SELECTED_SHA="$(git rev-parse HEAD)"
git fetch --no-tags origin '+refs/heads/*:refs/remotes/origin/*'
git fetch --tags origin '+refs/tags/*:refs/tags/*'
if git tag --points-at "${SELECTED_SHA}" | grep -Eq '^v'; then
exit 0
RELEASE_BRANCH_REF="refs/remotes/origin/${WORKFLOW_REF_NAME}"
git fetch --no-tags origin "+refs/heads/${WORKFLOW_REF_NAME}:refs/remotes/origin/${WORKFLOW_REF_NAME}"
if [[ "${RELEASE_REF}" =~ ^[0-9a-fA-F]{40}$ ]]; then
BRANCH_SHA="$(git rev-parse "${RELEASE_BRANCH_REF}")"
if [[ "$(git rev-parse HEAD)" != "${BRANCH_SHA}" ]]; then
echo "Commit SHA mode only supports the current ${WORKFLOW_REF_NAME} HEAD. Use a release tag for older commits." >&2
exit 1
fi
else
git merge-base --is-ancestor HEAD "${RELEASE_BRANCH_REF}"
fi
if git for-each-ref --format='%(refname:short)' --contains "${SELECTED_SHA}" refs/remotes/origin | grep -Eq '^origin/'; then
exit 0
fi
echo "Ref '${RELEASE_REF}' resolved to ${SELECTED_SHA}, but that commit is not reachable from an OpenClaw branch or release tag." >&2
echo "Secret-bearing release checks only run repository-owned branch/tag history, not arbitrary unreferenced commits." >&2
exit 1
- name: Finalize resolved SHA
id: ref
env:
FAST_SHA: ${{ steps.fast_ref.outputs.sha }}
FALLBACK_SHA: ${{ steps.fallback_ref.outputs.sha }}
EXPECTED_SHA: ${{ inputs.expected_sha }}
USED_FALLBACK: ${{ steps.fast_ref.outputs.fallback }}
run: |
set -euo pipefail
selected_sha="$FAST_SHA"
if [[ "$USED_FALLBACK" == "true" ]]; then
selected_sha="$FALLBACK_SHA"
fi
if [[ -z "$selected_sha" ]]; then
echo "Failed to resolve selected ref SHA." >&2
exit 1
fi
if [[ -n "${EXPECTED_SHA// }" ]] && [[ "${selected_sha,,}" != "${EXPECTED_SHA,,}" ]]; then
echo "Ref resolved to ${selected_sha}, expected ${EXPECTED_SHA}." >&2
exit 1
fi
echo "sha=${selected_sha,,}" >> "$GITHUB_OUTPUT"
- name: Capture selected inputs
id: inputs
env:
RELEASE_REF_INPUT: ${{ inputs.ref }}
RELEASE_PROVIDER_INPUT: ${{ inputs.provider }}
RELEASE_MODE_INPUT: ${{ inputs.mode }}
RELEASE_PROFILE_INPUT: ${{ inputs.release_profile }}
RELEASE_RERUN_GROUP_INPUT: ${{ inputs.rerun_group }}
RELEASE_LIVE_SUITE_FILTER_INPUT: ${{ inputs.live_suite_filter }}
run: |
set -euo pipefail
{
printf 'ref=%s\n' "$RELEASE_REF_INPUT"
printf 'provider=%s\n' "$RELEASE_PROVIDER_INPUT"
printf 'mode=%s\n' "$RELEASE_MODE_INPUT"
printf 'release_profile=%s\n' "$RELEASE_PROFILE_INPUT"
printf 'rerun_group=%s\n' "$RELEASE_RERUN_GROUP_INPUT"
printf 'live_suite_filter=%s\n' "$RELEASE_LIVE_SUITE_FILTER_INPUT"
} >> "$GITHUB_OUTPUT"
- name: Summarize validated ref
env:
RELEASE_REF: ${{ inputs.ref }}
RELEASE_SHA: ${{ steps.ref.outputs.sha }}
RELEASE_REF_FAST_PATH: ${{ steps.fast_ref.outputs.fast }}
RELEASE_PROVIDER: ${{ inputs.provider }}
RELEASE_MODE: ${{ inputs.mode }}
RELEASE_PROFILE: ${{ inputs.release_profile }}
RELEASE_RERUN_GROUP: ${{ inputs.rerun_group }}
RELEASE_LIVE_SUITE_FILTER: ${{ inputs.live_suite_filter }}
run: |
{
echo "## Release checks"
echo
echo "- Requested ref: \`${RELEASE_REF}\`"
echo "- Validated SHA: \`${RELEASE_SHA}\`"
echo "- Ref resolution fast path: \`${RELEASE_REF_FAST_PATH}\`"
echo "- Cross-OS provider: \`${RELEASE_PROVIDER}\`"
echo "- Cross-OS mode: \`${RELEASE_MODE}\`"
echo "- Release profile: \`${RELEASE_PROFILE}\`"
echo "- Rerun group: \`${RELEASE_RERUN_GROUP}\`"
if [[ -n "${RELEASE_LIVE_SUITE_FILTER// }" ]]; then
echo "- Live suite filter: \`${RELEASE_LIVE_SUITE_FILTER}\`"
fi
echo "- This run will execute cross-OS release validation, install smoke, QA Lab parity, Matrix, and Telegram lanes, and the non-Parallels Docker/live/openwebui coverage from the CI migration plan."
} >> "$GITHUB_STEP_SUMMARY"
prepare_release_package:
name: Prepare release package artifact
needs: [resolve_target]
if: contains(fromJSON('["all","cross-os","package"]'), needs.resolve_target.outputs.rerun_group) || (needs.resolve_target.outputs.rerun_group == 'live-e2e' && needs.resolve_target.outputs.live_suite_filter == '')
runs-on: ubuntu-24.04
timeout-minutes: 60
permissions:
contents: read
packages: write
outputs:
artifact_name: ${{ steps.artifact.outputs.name }}
package_sha256: ${{ steps.package.outputs.sha256 }}
package_version: ${{ steps.package.outputs.package_version }}
source_sha: ${{ steps.package.outputs.source_sha }}
steps:
- name: Checkout trusted workflow ref
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ github.ref_name }}
fetch-depth: 0
- name: Set artifact metadata
id: artifact
run: echo "name=release-package-under-test" >> "$GITHUB_OUTPUT"
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
install-deps: "false"
- name: Resolve release package artifact
id: package
shell: bash
env:
PACKAGE_REF: ${{ needs.resolve_target.outputs.revision }}
run: |
set -euo pipefail
node scripts/resolve-openclaw-package-candidate.mjs \
--source ref \
--package-ref "$PACKAGE_REF" \
--output-dir .artifacts/docker-e2e-package \
--output-name openclaw-current.tgz \
--metadata .artifacts/docker-e2e-package/package-candidate.json \
--github-output "$GITHUB_OUTPUT"
digest="$(node -p "JSON.parse(require('fs').readFileSync('.artifacts/docker-e2e-package/package-candidate.json', 'utf8')).sha256")"
version="$(node -p "JSON.parse(require('fs').readFileSync('.artifacts/docker-e2e-package/package-candidate.json', 'utf8')).version")"
source_sha="$(node -p "JSON.parse(require('fs').readFileSync('.artifacts/docker-e2e-package/package-candidate.json', 'utf8')).packageSourceSha")"
echo "source_sha=$source_sha" >> "$GITHUB_OUTPUT"
{
echo "## Release package artifact"
echo
echo "- Artifact: \`release-package-under-test\`"
echo "- Package ref: \`$PACKAGE_REF\`"
echo "- SHA-256: \`$digest\`"
echo "- Version: \`$version\`"
echo "- Source SHA: \`$source_sha\`"
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload release package artifact
uses: actions/upload-artifact@v7
with:
name: release-package-under-test
path: .artifacts/docker-e2e-package/openclaw-current.tgz
retention-days: 14
if-no-files-found: error
install_smoke_release_checks:
needs: [resolve_target]
if: contains(fromJSON('["all","install-smoke"]'), needs.resolve_target.outputs.rerun_group)
permissions:
contents: read
packages: write
uses: ./.github/workflows/install-smoke.yml
with:
ref: ${{ needs.resolve_target.outputs.revision }}
ref: ${{ needs.resolve_target.outputs.ref }}
run_bun_global_install_smoke: true
cross_os_release_checks:
needs: [resolve_target, prepare_release_package]
if: contains(fromJSON('["all","cross-os"]'), needs.resolve_target.outputs.rerun_group)
needs: [resolve_target]
permissions: read-all
uses: ./.github/workflows/openclaw-cross-os-release-checks-reusable.yml
with:
ref: ${{ needs.resolve_target.outputs.revision }}
ref: ${{ needs.resolve_target.outputs.ref }}
provider: ${{ needs.resolve_target.outputs.provider }}
mode: ${{ needs.resolve_target.outputs.mode }}
candidate_artifact_name: ${{ needs.prepare_release_package.outputs.artifact_name }}
candidate_file_name: openclaw-current.tgz
candidate_version: ${{ needs.prepare_release_package.outputs.package_version }}
candidate_source_sha: ${{ needs.prepare_release_package.outputs.source_sha }}
secrets:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
@@ -339,110 +152,19 @@ jobs:
OPENCLAW_DISCORD_SMOKE_GUILD_ID: ${{ secrets.OPENCLAW_DISCORD_SMOKE_GUILD_ID }}
OPENCLAW_DISCORD_SMOKE_CHANNEL_ID: ${{ secrets.OPENCLAW_DISCORD_SMOKE_CHANNEL_ID }}
live_repo_e2e_release_checks:
name: Run repo/live E2E validation
live_and_e2e_release_checks:
needs: [resolve_target]
if: contains(fromJSON('["all","live-e2e"]'), needs.resolve_target.outputs.rerun_group)
permissions:
actions: read
contents: read
packages: write
pull-requests: read
uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml
with:
ref: ${{ needs.resolve_target.outputs.revision }}
ref: ${{ needs.resolve_target.outputs.ref }}
include_repo_e2e: true
include_release_path_suites: false
include_openwebui: false
include_live_suites: true
release_test_profile: ${{ needs.resolve_target.outputs.release_profile }}
live_suite_filter: ${{ needs.resolve_target.outputs.live_suite_filter }}
secrets: &live_e2e_release_secrets
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
MODELSTUDIO_API_KEY: ${{ secrets.MODELSTUDIO_API_KEY }}
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
OPENCODE_ZEN_API_KEY: ${{ secrets.OPENCODE_ZEN_API_KEY }}
OPENCLAW_LIVE_BROWSER_CDP_URL: ${{ secrets.OPENCLAW_LIVE_BROWSER_CDP_URL }}
OPENCLAW_LIVE_SETUP_TOKEN: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN }}
OPENCLAW_LIVE_SETUP_TOKEN_MODEL: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_MODEL }}
OPENCLAW_LIVE_SETUP_TOKEN_PROFILE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_PROFILE }}
OPENCLAW_LIVE_SETUP_TOKEN_VALUE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_VALUE }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
FAL_KEY: ${{ secrets.FAL_KEY }}
RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }}
DEEPGRAM_API_KEY: ${{ secrets.DEEPGRAM_API_KEY }}
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
VYDRA_API_KEY: ${{ secrets.VYDRA_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
BYTEPLUS_ACCESS_KEY_ID: ${{ secrets.BYTEPLUS_ACCESS_KEY_ID }}
BYTEPLUS_SECRET_ACCESS_KEY: ${{ secrets.BYTEPLUS_SECRET_ACCESS_KEY }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
OPENCLAW_CODEX_AUTH_JSON: ${{ secrets.OPENCLAW_CODEX_AUTH_JSON }}
OPENCLAW_CODEX_CONFIG_TOML: ${{ secrets.OPENCLAW_CODEX_CONFIG_TOML }}
OPENCLAW_CLAUDE_JSON: ${{ secrets.OPENCLAW_CLAUDE_JSON }}
OPENCLAW_CLAUDE_CREDENTIALS_JSON: ${{ secrets.OPENCLAW_CLAUDE_CREDENTIALS_JSON }}
OPENCLAW_CLAUDE_SETTINGS_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_JSON }}
OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON }}
OPENCLAW_GEMINI_SETTINGS_JSON: ${{ secrets.OPENCLAW_GEMINI_SETTINGS_JSON }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
docker_e2e_release_checks:
name: Run Docker release-path validation
needs: [resolve_target, prepare_release_package]
if: contains(fromJSON('["all","live-e2e"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.live_suite_filter == ''
permissions:
actions: read
contents: read
packages: write
pull-requests: read
uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml
with:
ref: ${{ needs.resolve_target.outputs.revision }}
include_repo_e2e: false
include_release_path_suites: true
include_openwebui: ${{ needs.resolve_target.outputs.release_profile != 'minimum' }}
include_live_suites: false
release_test_profile: ${{ needs.resolve_target.outputs.release_profile }}
package_artifact_name: ${{ needs.prepare_release_package.outputs.artifact_name }}
secrets: *live_e2e_release_secrets
package_acceptance_release_checks:
name: Run package acceptance
needs: [resolve_target, prepare_release_package]
if: contains(fromJSON('["all","package"]'), needs.resolve_target.outputs.rerun_group)
permissions:
actions: read
contents: read
packages: write
pull-requests: read
uses: ./.github/workflows/package-acceptance.yml
with:
workflow_ref: ${{ github.ref_name }}
source: artifact
artifact_name: ${{ needs.prepare_release_package.outputs.artifact_name }}
package_sha256: ${{ needs.prepare_release_package.outputs.package_sha256 }}
suite_profile: custom
docker_lanes: bundled-channel-deps-compat plugins-offline
telegram_mode: mock-openai
telegram_scenarios: telegram-help-command,telegram-commands-command,telegram-tools-compact-command,telegram-whoami-command,telegram-context-command,telegram-mention-gating
include_openwebui: true
include_live_suites: true
secrets:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
@@ -451,7 +173,6 @@ jobs:
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
@@ -489,25 +210,14 @@ jobs:
OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON }}
OPENCLAW_GEMINI_SETTINGS_JSON: ${{ secrets.OPENCLAW_GEMINI_SETTINGS_JSON }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
qa_lab_parity_lane_release_checks:
name: Run QA Lab parity lane (${{ matrix.lane }})
qa_lab_parity_release_checks:
name: Run QA Lab parity gate
needs: [resolve_target]
if: contains(fromJSON('["all","qa","qa-parity"]'), needs.resolve_target.outputs.rerun_group)
runs-on: blacksmith-8vcpu-ubuntu-2404
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 30
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- lane: candidate
output_dir: gpt54
- lane: baseline
output_dir: opus46
env:
QA_PARITY_CONCURRENCY: "1"
OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS: "180000"
@@ -523,8 +233,7 @@ jobs:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ needs.resolve_target.outputs.revision }}
ref: ${{ needs.resolve_target.outputs.ref }}
fetch-depth: 1
- name: Setup Node environment
@@ -537,81 +246,25 @@ jobs:
- name: Build private QA runtime
run: pnpm build
- name: Run parity lane
env:
QA_PARITY_LANE: ${{ matrix.lane }}
QA_PARITY_OUTPUT_DIR: ${{ matrix.output_dir }}
- name: Run OpenAI candidate lane
run: |
set -euo pipefail
case "${QA_PARITY_LANE}" in
candidate)
model="${OPENCLAW_CI_OPENAI_MODEL}"
alt_model="openai/gpt-5.4-alt"
;;
baseline)
model="anthropic/claude-opus-4-6"
alt_model="anthropic/claude-sonnet-4-6"
;;
*)
echo "Unknown QA parity lane: ${QA_PARITY_LANE}" >&2
exit 1
;;
esac
pnpm openclaw qa suite \
--provider-mode mock-openai \
--parity-pack agentic \
--concurrency "${QA_PARITY_CONCURRENCY}" \
--model "${model}" \
--alt-model "${alt_model}" \
--output-dir ".artifacts/qa-e2e/${QA_PARITY_OUTPUT_DIR}"
--model "${OPENCLAW_CI_OPENAI_MODEL}" \
--alt-model openai/gpt-5.4-alt \
--output-dir .artifacts/qa-e2e/gpt54
- name: Upload parity lane artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: release-qa-parity-${{ matrix.lane }}-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
retention-days: 14
if-no-files-found: warn
qa_lab_parity_report_release_checks:
name: Run QA Lab parity report
needs: [resolve_target, qa_lab_parity_lane_release_checks]
if: contains(fromJSON('["all","qa","qa-parity"]'), needs.resolve_target.outputs.rerun_group)
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 20
permissions:
contents: read
actions: read
env:
OPENCLAW_BUILD_PRIVATE_QA: "1"
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ needs.resolve_target.outputs.revision }}
fetch-depth: 1
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Download parity lane artifacts
uses: actions/download-artifact@v4
with:
pattern: release-qa-parity-*-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
merge-multiple: true
- name: Build private QA runtime
run: pnpm build
- name: Run Opus 4.6 lane
run: |
pnpm openclaw qa suite \
--provider-mode mock-openai \
--parity-pack agentic \
--concurrency "${QA_PARITY_CONCURRENCY}" \
--model anthropic/claude-opus-4-6 \
--alt-model anthropic/claude-sonnet-4-6 \
--output-dir .artifacts/qa-e2e/opus46
- name: Generate parity report
run: |
@@ -627,7 +280,7 @@ jobs:
if: always()
uses: actions/upload-artifact@v4
with:
name: release-qa-parity-${{ needs.resolve_target.outputs.revision }}
name: release-qa-parity-${{ needs.resolve_target.outputs.sha }}
path: .artifacts/qa-e2e/
retention-days: 14
if-no-files-found: warn
@@ -635,8 +288,7 @@ jobs:
qa_live_matrix_release_checks:
name: Run QA Lab live Matrix lane
needs: [resolve_target]
if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group)
runs-on: blacksmith-8vcpu-ubuntu-2404
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 60
permissions:
contents: read
@@ -649,85 +301,7 @@ jobs:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ needs.resolve_target.outputs.revision }}
fetch-depth: 1
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Build private QA runtime
run: pnpm build
- name: Run Matrix live lane
id: run_lane
shell: bash
env:
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
OPENCLAW_QA_MATRIX_CANARY_TIMEOUT_MS: "90000"
OPENCLAW_QA_MATRIX_NO_REPLY_WINDOW_MS: "3000"
run: |
set -euo pipefail
output_dir=".artifacts/qa-e2e/matrix-live-release-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
echo "output_dir=${output_dir}" >> "$GITHUB_OUTPUT"
matrix_args=(
--repo-root . \
--provider-mode mock-openai \
--model mock-openai/gpt-5.5 \
--alt-model mock-openai/gpt-5.5-alt \
--profile fast \
--fast
)
if pnpm openclaw qa matrix --help 2>/dev/null | grep -F -q -- "--fail-fast"; then
matrix_args+=(--fail-fast)
fi
for attempt in 1 2; do
attempt_output_dir="${output_dir}/attempt-${attempt}"
if pnpm openclaw qa matrix --output-dir "${attempt_output_dir}" "${matrix_args[@]}"; then
exit 0
fi
if [[ "${attempt}" == "2" ]]; then
exit 1
fi
echo "Matrix live lane failed on attempt ${attempt}; retrying once..." >&2
sleep 10
done
- name: Upload Matrix QA artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: release-qa-live-matrix-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
retention-days: 14
if-no-files-found: warn
qa_live_telegram_release_checks:
name: Run QA Lab live Telegram lane
needs: [resolve_target]
if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group)
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 60
permissions:
contents: read
pull-requests: read
environment: qa-live-shared
env:
OPENCLAW_BUILD_PRIVATE_QA: "1"
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ needs.resolve_target.outputs.revision }}
ref: ${{ needs.resolve_target.outputs.ref }}
fetch-depth: 1
- name: Setup Node environment
@@ -739,6 +313,77 @@ jobs:
- name: Validate required QA credential env
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
shell: bash
run: |
set -euo pipefail
if [[ -z "${OPENAI_API_KEY:-}" ]]; then
echo "Missing required OPENAI_API_KEY." >&2
exit 1
fi
- name: Build private QA runtime
run: pnpm build
- name: Run Matrix live lane
id: run_lane
shell: bash
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
run: |
set -euo pipefail
output_dir=".artifacts/qa-e2e/matrix-live-release-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
echo "output_dir=${output_dir}" >> "$GITHUB_OUTPUT"
pnpm openclaw qa matrix \
--repo-root . \
--output-dir "${output_dir}" \
--provider-mode live-frontier \
--model "${OPENCLAW_CI_OPENAI_MODEL}" \
--alt-model "${OPENCLAW_CI_OPENAI_MODEL}" \
--fast
- name: Upload Matrix QA artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: release-qa-live-matrix-${{ needs.resolve_target.outputs.sha }}
path: ${{ steps.run_lane.outputs.output_dir }}
retention-days: 14
if-no-files-found: warn
qa_live_telegram_release_checks:
name: Run QA Lab live Telegram lane
needs: [resolve_target]
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 60
permissions:
contents: read
pull-requests: read
environment: qa-live-shared
env:
OPENCLAW_BUILD_PRIVATE_QA: "1"
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
ref: ${{ needs.resolve_target.outputs.ref }}
fetch-depth: 1
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Validate required QA credential env
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
shell: bash
@@ -753,6 +398,7 @@ jobs:
fi
}
require_var OPENAI_API_KEY
require_var OPENCLAW_QA_CONVEX_SITE_URL
require_var OPENCLAW_QA_CONVEX_SECRET_CI
@@ -763,6 +409,7 @@ jobs:
id: run_lane
shell: bash
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
@@ -773,75 +420,21 @@ jobs:
output_dir=".artifacts/qa-e2e/telegram-live-release-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
echo "output_dir=${output_dir}" >> "$GITHUB_OUTPUT"
for attempt in 1 2; do
attempt_output_dir="${output_dir}/attempt-${attempt}"
if pnpm openclaw qa telegram \
--repo-root . \
--output-dir "${attempt_output_dir}" \
--provider-mode mock-openai \
--model mock-openai/gpt-5.5 \
--alt-model mock-openai/gpt-5.5-alt \
--fast \
--credential-source convex \
--credential-role ci; then
exit 0
fi
if [[ "${attempt}" == "2" ]]; then
exit 1
fi
echo "Telegram live lane failed on attempt ${attempt}; retrying once..." >&2
sleep 10
done
pnpm openclaw qa telegram \
--repo-root . \
--output-dir "${output_dir}" \
--provider-mode live-frontier \
--model "${OPENCLAW_CI_OPENAI_MODEL}" \
--alt-model "${OPENCLAW_CI_OPENAI_MODEL}" \
--fast \
--credential-source convex \
--credential-role ci
- name: Upload Telegram QA artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: release-qa-live-telegram-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
name: release-qa-live-telegram-${{ needs.resolve_target.outputs.sha }}
path: ${{ steps.run_lane.outputs.output_dir }}
retention-days: 14
if-no-files-found: warn
summary:
name: Verify release checks
needs:
- prepare_release_package
- install_smoke_release_checks
- cross_os_release_checks
- live_repo_e2e_release_checks
- docker_e2e_release_checks
- package_acceptance_release_checks
- qa_lab_parity_lane_release_checks
- qa_lab_parity_report_release_checks
- qa_live_matrix_release_checks
- qa_live_telegram_release_checks
if: always()
runs-on: ubuntu-24.04
permissions: {}
timeout-minutes: 5
steps:
- name: Verify release check results
shell: bash
run: |
set -euo pipefail
failed=0
for item in \
"prepare_release_package=${{ needs.prepare_release_package.result }}" \
"install_smoke_release_checks=${{ needs.install_smoke_release_checks.result }}" \
"cross_os_release_checks=${{ needs.cross_os_release_checks.result }}" \
"live_repo_e2e_release_checks=${{ needs.live_repo_e2e_release_checks.result }}" \
"docker_e2e_release_checks=${{ needs.docker_e2e_release_checks.result }}" \
"package_acceptance_release_checks=${{ needs.package_acceptance_release_checks.result }}" \
"qa_lab_parity_lane_release_checks=${{ needs.qa_lab_parity_lane_release_checks.result }}" \
"qa_lab_parity_report_release_checks=${{ needs.qa_lab_parity_report_release_checks.result }}" \
"qa_live_matrix_release_checks=${{ needs.qa_live_matrix_release_checks.result }}" \
"qa_live_telegram_release_checks=${{ needs.qa_live_telegram_release_checks.result }}"
do
name="${item%%=*}"
result="${item#*=}"
if [[ "$result" != "success" && "$result" != "skipped" ]]; then
echo "::error::${name} ended with ${result}"
failed=1
fi
done
exit "$failed"

View File

@@ -38,7 +38,6 @@ jobs:
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}

View File

@@ -1,70 +0,0 @@
name: OpenGrep — Full
# Manual repository-wide scan for the high-precision OpenGrep rule super-config.
# This is intentionally separate from PR scanning so broad/backlog findings do
# not block unrelated pull requests.
on:
workflow_dispatch:
concurrency:
group: opengrep-full-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
permissions:
contents: read
security-events: write
jobs:
scan:
name: Scan full repository (precise)
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Install opengrep
env:
# Pin both the install script (by commit SHA) and the binary version.
# The script SHA must match the v1.19.0 release tag in opengrep/opengrep
# so a compromised or force-pushed `main` cannot RCE in our CI runner.
# Bump both together when upgrading.
OPENGREP_VERSION: v1.19.0
OPENGREP_INSTALL_SHA: 9a4c0a68220618441608cd2bad4ff2eddccf8113
run: |
curl -fsSL "https://raw.githubusercontent.com/opengrep/opengrep/${OPENGREP_INSTALL_SHA}/install.sh" \
| bash -s -- -v "$OPENGREP_VERSION"
echo "$HOME/.opengrep/cli/latest" >> "$GITHUB_PATH"
- name: Verify opengrep
run: opengrep --version
- name: Run full opengrep scan
# Manual full scans cover all first-party source paths so maintainers can
# audit the complete rulepack without making PRs inherit unrelated backlog.
run: |
mkdir -p .opengrep-out
scripts/run-opengrep.sh --sarif --error
- name: Upload SARIF to GitHub Code Scanning
uses: github/codeql-action/upload-sarif@v4
# Only upload if the scan actually produced a SARIF file.
if: always() && hashFiles('.opengrep-out/precise.sarif') != ''
with:
sarif_file: .opengrep-out/precise.sarif
category: opengrep-full
- name: Upload SARIF as workflow artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: opengrep-full-sarif
path: .opengrep-out/precise.sarif
if-no-files-found: warn
retention-days: 30

View File

@@ -1,100 +0,0 @@
name: OpenGrep — PR Diff
# Runs the high-precision OpenGrep rule super-config against only first-party
# source paths changed by a pull request. Keeping PR scans diff-scoped makes
# findings attributable to the proposed change instead of surfacing unrelated
# repository-wide backlog.
#
# For a repository-wide scan, use the manual OpenGrep — Full workflow.
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- ".github/actions/ensure-base-commit/**"
- ".github/workflows/opengrep-precise.yml"
- ".github/workflows/opengrep-precise-full.yml"
- ".semgrepignore"
- "apps/**"
- "extensions/**"
- "packages/**"
- "scripts/**"
- "security/opengrep/**"
- "src/**"
concurrency:
group: opengrep-pr-diff-${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
permissions:
contents: read
security-events: write
jobs:
scan:
name: Scan changed paths (precise)
if: ${{ !github.event.pull_request.draft }}
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ github.sha }}
fetch-depth: 1
fetch-tags: false
persist-credentials: false
submodules: false
- name: Ensure PR base commit
uses: ./.github/actions/ensure-base-commit
with:
base-sha: ${{ github.event.pull_request.base.sha }}
fetch-ref: ${{ github.event.pull_request.base.ref }}
- name: Install opengrep
env:
# Pin both the install script (by commit SHA) and the binary version.
# The script SHA must match the v1.19.0 release tag in opengrep/opengrep
# so a compromised or force-pushed `main` cannot RCE in our CI runner.
# Bump both together when upgrading.
OPENGREP_VERSION: v1.19.0
OPENGREP_INSTALL_SHA: 9a4c0a68220618441608cd2bad4ff2eddccf8113
run: |
curl -fsSL "https://raw.githubusercontent.com/opengrep/opengrep/${OPENGREP_INSTALL_SHA}/install.sh" \
| bash -s -- -v "$OPENGREP_VERSION"
echo "$HOME/.opengrep/cli/latest" >> "$GITHUB_PATH"
- name: Verify opengrep
run: opengrep --version
- name: Run opengrep on PR diff
env:
OPENCLAW_OPENGREP_BASE_REF: ${{ github.event.pull_request.base.sha }}...HEAD
# Findings from precise rules block this workflow. Pull requests scan
# changed first-party source paths only so findings stay attributable to
# the PR diff. Test/fixture/QA path exclusions live in `.semgrepignore`
# at the repo root and are picked up automatically.
run: |
mkdir -p .opengrep-out
scripts/run-opengrep.sh --changed --sarif --error
- name: Upload SARIF to GitHub Code Scanning
uses: github/codeql-action/upload-sarif@v4
# Only upload if the scan actually produced a SARIF file.
if: always() && hashFiles('.opengrep-out/precise.sarif') != ''
with:
sarif_file: .opengrep-out/precise.sarif
category: opengrep-pr-diff
- name: Upload SARIF as workflow artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: opengrep-pr-diff-sarif
path: .opengrep-out/precise.sarif
if-no-files-found: warn
retention-days: 30

View File

@@ -1,615 +0,0 @@
name: Package Acceptance
on:
workflow_dispatch:
inputs:
workflow_ref:
description: Trusted repo ref for workflow scripts and Docker E2E harness
required: true
default: main
type: string
source:
description: Package candidate source
required: true
default: npm
type: choice
options:
- npm
- ref
- url
- artifact
package_ref:
description: Trusted package source ref when source=ref
required: true
default: main
type: string
package_spec:
description: Published package spec when source=npm
required: false
default: openclaw@beta
type: string
package_url:
description: HTTPS .tgz URL when source=url
required: false
default: ""
type: string
package_sha256:
description: Expected package SHA-256; required for source=url
required: false
default: ""
type: string
artifact_run_id:
description: GitHub Actions run id when source=artifact
required: false
default: ""
type: string
artifact_name:
description: Artifact name containing one .tgz when source=artifact
required: false
default: package-under-test
type: string
suite_profile:
description: Acceptance profile
required: true
default: package
type: choice
options:
- smoke
- package
- product
- full
- custom
docker_lanes:
description: Comma/space separated Docker lanes when suite_profile=custom
required: false
default: ""
type: string
published_upgrade_survivor_baseline:
description: Published OpenClaw package baseline for the published-upgrade-survivor Docker lane
required: false
default: openclaw@latest
type: string
published_upgrade_survivor_baselines:
description: Optional baseline list for published-upgrade-survivor; use release-history for last 6 plus key legacy releases
required: false
default: ""
type: string
published_upgrade_survivor_scenarios:
description: Optional scenario list for published-upgrade-survivor; use reported-issues for known upgrade failure shapes
required: false
default: ""
type: string
telegram_mode:
description: Optional Telegram QA lane for the resolved package candidate
required: true
default: none
type: choice
options:
- none
- mock-openai
- live-frontier
telegram_scenarios:
description: Optional comma-separated Telegram scenario ids
required: false
default: ""
type: string
workflow_call:
inputs:
workflow_ref:
description: Trusted repo ref for workflow scripts and Docker E2E harness
required: false
default: main
type: string
source:
description: "Package candidate source: npm, ref, url, or artifact"
required: true
type: string
package_ref:
description: Trusted package source ref when source=ref
required: false
default: main
type: string
package_spec:
description: Published package spec when source=npm
required: false
default: openclaw@beta
type: string
package_url:
description: HTTPS .tgz URL when source=url
required: false
default: ""
type: string
package_sha256:
description: Expected package SHA-256; required for source=url
required: false
default: ""
type: string
artifact_run_id:
description: GitHub Actions run id when source=artifact
required: false
default: ""
type: string
artifact_name:
description: Artifact name containing one .tgz when source=artifact
required: false
default: package-under-test
type: string
suite_profile:
description: "Acceptance profile: smoke, package, product, full, or custom"
required: false
default: package
type: string
docker_lanes:
description: Comma/space separated Docker lanes when suite_profile=custom
required: false
default: ""
type: string
published_upgrade_survivor_baseline:
description: Published OpenClaw package baseline for the published-upgrade-survivor Docker lane
required: false
default: openclaw@latest
type: string
published_upgrade_survivor_baselines:
description: Optional baseline list for published-upgrade-survivor; use release-history for last 6 plus key legacy releases
required: false
default: ""
type: string
published_upgrade_survivor_scenarios:
description: Optional scenario list for published-upgrade-survivor; use reported-issues for known upgrade failure shapes
required: false
default: ""
type: string
telegram_mode:
description: Optional Telegram QA lane for the resolved package candidate
required: false
default: none
type: string
telegram_scenarios:
description: Optional comma-separated Telegram scenario ids
required: false
default: ""
type: string
secrets:
OPENAI_API_KEY:
required: false
OPENAI_BASE_URL:
required: false
ANTHROPIC_API_KEY:
required: false
ANTHROPIC_API_KEY_OLD:
required: false
ANTHROPIC_API_TOKEN:
required: false
BYTEPLUS_API_KEY:
required: false
CEREBRAS_API_KEY:
required: false
DEEPINFRA_API_KEY:
required: false
DASHSCOPE_API_KEY:
required: false
GROQ_API_KEY:
required: false
KIMI_API_KEY:
required: false
MODELSTUDIO_API_KEY:
required: false
MOONSHOT_API_KEY:
required: false
MISTRAL_API_KEY:
required: false
MINIMAX_API_KEY:
required: false
OPENCODE_API_KEY:
required: false
OPENCODE_ZEN_API_KEY:
required: false
OPENCLAW_LIVE_BROWSER_CDP_URL:
required: false
OPENCLAW_LIVE_SETUP_TOKEN:
required: false
OPENCLAW_LIVE_SETUP_TOKEN_MODEL:
required: false
OPENCLAW_LIVE_SETUP_TOKEN_PROFILE:
required: false
OPENCLAW_LIVE_SETUP_TOKEN_VALUE:
required: false
GEMINI_API_KEY:
required: false
GOOGLE_API_KEY:
required: false
OPENROUTER_API_KEY:
required: false
QWEN_API_KEY:
required: false
FAL_KEY:
required: false
RUNWAY_API_KEY:
required: false
DEEPGRAM_API_KEY:
required: false
TOGETHER_API_KEY:
required: false
VYDRA_API_KEY:
required: false
XAI_API_KEY:
required: false
ZAI_API_KEY:
required: false
Z_AI_API_KEY:
required: false
BYTEPLUS_ACCESS_KEY_ID:
required: false
BYTEPLUS_SECRET_ACCESS_KEY:
required: false
CLAUDE_CODE_OAUTH_TOKEN:
required: false
OPENCLAW_CODEX_AUTH_JSON:
required: false
OPENCLAW_CODEX_CONFIG_TOML:
required: false
OPENCLAW_CLAUDE_JSON:
required: false
OPENCLAW_CLAUDE_CREDENTIALS_JSON:
required: false
OPENCLAW_CLAUDE_SETTINGS_JSON:
required: false
OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON:
required: false
OPENCLAW_GEMINI_SETTINGS_JSON:
required: false
FIREWORKS_API_KEY:
required: false
OPENCLAW_QA_CONVEX_SITE_URL:
required: false
OPENCLAW_QA_CONVEX_SECRET_CI:
required: false
permissions:
actions: read
contents: read
packages: write
pull-requests: read
concurrency:
group: package-acceptance-${{ github.run_id }}
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "10.33.0"
PACKAGE_ARTIFACT_NAME: package-under-test
jobs:
resolve_package:
name: Resolve package candidate
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 60
outputs:
docker_lanes: ${{ steps.profile.outputs.docker_lanes }}
include_live_suites: ${{ steps.profile.outputs.include_live_suites }}
include_openwebui: ${{ steps.profile.outputs.include_openwebui }}
include_release_path_suites: ${{ steps.profile.outputs.include_release_path_suites }}
package_artifact_name: ${{ steps.profile.outputs.package_artifact_name }}
package_source_sha: ${{ steps.resolve.outputs.package_source_sha }}
package_sha256: ${{ steps.resolve.outputs.sha256 }}
package_version: ${{ steps.resolve.outputs.package_version }}
published_upgrade_survivor_baselines: ${{ steps.upgrade_survivor_baselines.outputs.baselines }}
published_upgrade_survivor_scenarios: ${{ inputs.published_upgrade_survivor_scenarios }}
telegram_enabled: ${{ steps.profile.outputs.telegram_enabled }}
telegram_mode: ${{ steps.profile.outputs.telegram_mode }}
steps:
- name: Checkout package workflow ref
uses: actions/checkout@v6
with:
ref: ${{ inputs.workflow_ref }}
fetch-depth: 0
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: ${{ inputs.source == 'ref' && 'true' || 'false' }}
install-deps: "false"
- name: Download current-run package artifact input
if: inputs.source == 'artifact' && inputs.artifact_run_id == ''
uses: actions/download-artifact@v8
with:
name: ${{ inputs.artifact_name }}
path: .artifacts/package-candidate-input
- name: Download previous-run package artifact input
if: inputs.source == 'artifact' && inputs.artifact_run_id != ''
env:
GH_TOKEN: ${{ github.token }}
ARTIFACT_RUN_ID: ${{ inputs.artifact_run_id }}
ARTIFACT_NAME: ${{ inputs.artifact_name }}
shell: bash
run: |
set -euo pipefail
if [[ -z "${ARTIFACT_NAME// }" ]]; then
echo "artifact_name is required when source=artifact." >&2
exit 1
fi
mkdir -p .artifacts/package-candidate-input
gh run download "$ARTIFACT_RUN_ID" -n "$ARTIFACT_NAME" -D .artifacts/package-candidate-input
- name: Resolve package candidate
id: resolve
env:
SOURCE: ${{ inputs.source }}
PACKAGE_REF: ${{ inputs.package_ref }}
PACKAGE_SPEC: ${{ inputs.package_spec }}
PACKAGE_URL: ${{ inputs.package_url }}
PACKAGE_SHA256: ${{ inputs.package_sha256 }}
shell: bash
run: |
set -euo pipefail
artifact_dir=""
if [[ "$SOURCE" == "artifact" ]]; then
artifact_dir=".artifacts/package-candidate-input"
fi
node scripts/resolve-openclaw-package-candidate.mjs \
--source "$SOURCE" \
--package-ref "$PACKAGE_REF" \
--package-spec "$PACKAGE_SPEC" \
--package-url "$PACKAGE_URL" \
--package-sha256 "$PACKAGE_SHA256" \
--artifact-dir "${artifact_dir:-.}" \
--output-dir .artifacts/docker-e2e-package \
--output-name openclaw-current.tgz \
--metadata .artifacts/docker-e2e-package/package-candidate.json \
--github-output "$GITHUB_OUTPUT"
- name: Select acceptance profile
id: profile
env:
SOURCE: ${{ inputs.source }}
SUITE_PROFILE: ${{ inputs.suite_profile }}
CUSTOM_DOCKER_LANES: ${{ inputs.docker_lanes }}
TELEGRAM_MODE: ${{ inputs.telegram_mode }}
shell: bash
run: |
set -euo pipefail
include_release_path_suites=false
include_openwebui=false
include_live_suites=false
docker_lanes=""
case "$SUITE_PROFILE" in
smoke)
docker_lanes="npm-onboard-channel-agent gateway-network config-reload"
;;
package)
docker_lanes="npm-onboard-channel-agent doctor-switch update-channel-switch upgrade-survivor published-upgrade-survivor bundled-channel-deps-compat plugins-offline plugin-update"
;;
product)
docker_lanes="npm-onboard-channel-agent doctor-switch update-channel-switch upgrade-survivor published-upgrade-survivor bundled-channel-deps-compat plugins plugin-update mcp-channels cron-mcp-cleanup openai-web-search-minimal openwebui"
include_openwebui=true
;;
full)
include_release_path_suites=true
include_openwebui=true
;;
custom)
docker_lanes="$CUSTOM_DOCKER_LANES"
if [[ -z "${docker_lanes// }" ]]; then
echo "docker_lanes is required when suite_profile=custom." >&2
exit 1
fi
if [[ "$docker_lanes" == *"openwebui"* ]]; then
include_openwebui=true
fi
;;
*)
echo "Unknown suite_profile: $SUITE_PROFILE" >&2
exit 1
;;
esac
telegram_enabled=false
if [[ "$TELEGRAM_MODE" != "none" ]]; then
telegram_enabled=true
fi
{
echo "docker_lanes=$docker_lanes"
echo "include_release_path_suites=$include_release_path_suites"
echo "include_openwebui=$include_openwebui"
echo "include_live_suites=$include_live_suites"
echo "telegram_enabled=$telegram_enabled"
echo "telegram_mode=$TELEGRAM_MODE"
echo "package_artifact_name=${PACKAGE_ARTIFACT_NAME}"
} >> "$GITHUB_OUTPUT"
- name: Resolve published upgrade survivor baselines
id: upgrade_survivor_baselines
env:
FALLBACK_BASELINE: ${{ inputs.published_upgrade_survivor_baseline }}
REQUESTED_BASELINES: ${{ inputs.published_upgrade_survivor_baselines }}
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
if [[ -z "${REQUESTED_BASELINES// }" ]]; then
echo "baselines=" >> "$GITHUB_OUTPUT"
exit 0
fi
releases_json=""
npm_versions_json=""
if [[ "$REQUESTED_BASELINES" == *"release-history"* ]]; then
releases_json=".artifacts/package-candidate-input/openclaw-releases.json"
npm_versions_json=".artifacts/package-candidate-input/openclaw-npm-versions.json"
mkdir -p "$(dirname "$releases_json")"
gh release list --repo "$GITHUB_REPOSITORY" --limit 100 --json tagName,publishedAt,isPrerelease > "$releases_json"
npm view openclaw versions --json > "$npm_versions_json"
fi
args=(
--requested "$REQUESTED_BASELINES"
--fallback "$FALLBACK_BASELINE"
--github-output "$GITHUB_OUTPUT"
)
if [[ -n "$releases_json" ]]; then
args+=(
--releases-json "$releases_json"
--npm-versions-json "$npm_versions_json"
--history-count 6
--include-version 2026.4.23
--pre-date 2026-03-15T00:00:00Z
)
fi
node scripts/resolve-upgrade-survivor-baselines.mjs "${args[@]}" >/dev/null
- name: Upload package-under-test artifact
uses: actions/upload-artifact@v7
with:
name: ${{ env.PACKAGE_ARTIFACT_NAME }}
path: |
.artifacts/docker-e2e-package/openclaw-current.tgz
.artifacts/docker-e2e-package/package-candidate.json
retention-days: 14
if-no-files-found: error
- name: Summarize package candidate
env:
PACKAGE_SHA256: ${{ steps.resolve.outputs.sha256 }}
PACKAGE_VERSION: ${{ steps.resolve.outputs.package_version }}
PACKAGE_REF: ${{ inputs.package_ref }}
SOURCE: ${{ inputs.source }}
SUITE_PROFILE: ${{ inputs.suite_profile }}
WORKFLOW_REF: ${{ inputs.workflow_ref }}
PUBLISHED_UPGRADE_SURVIVOR_BASELINE: ${{ inputs.published_upgrade_survivor_baseline }}
PUBLISHED_UPGRADE_SURVIVOR_BASELINES: ${{ steps.upgrade_survivor_baselines.outputs.baselines }}
PUBLISHED_UPGRADE_SURVIVOR_SCENARIOS: ${{ inputs.published_upgrade_survivor_scenarios }}
shell: bash
run: |
{
echo "## Package acceptance"
echo
echo "- Source: \`${SOURCE}\`"
echo "- Workflow ref: \`${WORKFLOW_REF}\`"
if [[ "${SOURCE}" == "ref" ]]; then
echo "- Package ref: \`${PACKAGE_REF}\`"
fi
echo "- Version: \`${PACKAGE_VERSION}\`"
echo "- SHA-256: \`${PACKAGE_SHA256}\`"
echo "- Profile: \`${SUITE_PROFILE}\`"
echo "- Published upgrade survivor baseline: \`${PUBLISHED_UPGRADE_SURVIVOR_BASELINE}\`"
echo "- Published upgrade survivor baselines: \`${PUBLISHED_UPGRADE_SURVIVOR_BASELINES}\`"
echo "- Published upgrade survivor scenarios: \`${PUBLISHED_UPGRADE_SURVIVOR_SCENARIOS}\`"
} >> "$GITHUB_STEP_SUMMARY"
docker_acceptance:
name: Docker product acceptance
needs: resolve_package
uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml
with:
ref: ${{ inputs.workflow_ref }}
include_repo_e2e: false
include_release_path_suites: ${{ needs.resolve_package.outputs.include_release_path_suites == 'true' }}
include_openwebui: ${{ needs.resolve_package.outputs.include_openwebui == 'true' }}
docker_lanes: ${{ needs.resolve_package.outputs.docker_lanes }}
published_upgrade_survivor_baseline: ${{ inputs.published_upgrade_survivor_baseline }}
published_upgrade_survivor_baselines: ${{ needs.resolve_package.outputs.published_upgrade_survivor_baselines }}
published_upgrade_survivor_scenarios: ${{ needs.resolve_package.outputs.published_upgrade_survivor_scenarios }}
package_artifact_name: ${{ needs.resolve_package.outputs.package_artifact_name }}
include_live_suites: ${{ needs.resolve_package.outputs.include_live_suites == 'true' }}
live_models_only: false
secrets:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
MODELSTUDIO_API_KEY: ${{ secrets.MODELSTUDIO_API_KEY }}
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
OPENCODE_ZEN_API_KEY: ${{ secrets.OPENCODE_ZEN_API_KEY }}
OPENCLAW_LIVE_BROWSER_CDP_URL: ${{ secrets.OPENCLAW_LIVE_BROWSER_CDP_URL }}
OPENCLAW_LIVE_SETUP_TOKEN: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN }}
OPENCLAW_LIVE_SETUP_TOKEN_MODEL: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_MODEL }}
OPENCLAW_LIVE_SETUP_TOKEN_PROFILE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_PROFILE }}
OPENCLAW_LIVE_SETUP_TOKEN_VALUE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_VALUE }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
FAL_KEY: ${{ secrets.FAL_KEY }}
RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }}
DEEPGRAM_API_KEY: ${{ secrets.DEEPGRAM_API_KEY }}
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
VYDRA_API_KEY: ${{ secrets.VYDRA_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
BYTEPLUS_ACCESS_KEY_ID: ${{ secrets.BYTEPLUS_ACCESS_KEY_ID }}
BYTEPLUS_SECRET_ACCESS_KEY: ${{ secrets.BYTEPLUS_SECRET_ACCESS_KEY }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
OPENCLAW_CODEX_AUTH_JSON: ${{ secrets.OPENCLAW_CODEX_AUTH_JSON }}
OPENCLAW_CODEX_CONFIG_TOML: ${{ secrets.OPENCLAW_CODEX_CONFIG_TOML }}
OPENCLAW_CLAUDE_JSON: ${{ secrets.OPENCLAW_CLAUDE_JSON }}
OPENCLAW_CLAUDE_CREDENTIALS_JSON: ${{ secrets.OPENCLAW_CLAUDE_CREDENTIALS_JSON }}
OPENCLAW_CLAUDE_SETTINGS_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_JSON }}
OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON }}
OPENCLAW_GEMINI_SETTINGS_JSON: ${{ secrets.OPENCLAW_GEMINI_SETTINGS_JSON }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
package_telegram:
name: Telegram package acceptance
needs: resolve_package
if: needs.resolve_package.outputs.telegram_enabled == 'true'
uses: ./.github/workflows/npm-telegram-beta-e2e.yml
with:
package_spec: ${{ inputs.package_spec }}
package_artifact_name: ${{ needs.resolve_package.outputs.package_artifact_name }}
package_label: openclaw@${{ needs.resolve_package.outputs.package_version }}
harness_ref: ${{ needs.resolve_package.outputs.package_source_sha || inputs.workflow_ref }}
provider_mode: ${{ needs.resolve_package.outputs.telegram_mode }}
scenario: ${{ inputs.telegram_scenarios }}
secrets:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
summary:
name: Verify package acceptance
needs: [resolve_package, docker_acceptance, package_telegram]
if: always()
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 5
steps:
- name: Verify package acceptance results
env:
DOCKER_RESULT: ${{ needs.docker_acceptance.result }}
PACKAGE_TELEGRAM_RESULT: ${{ needs.package_telegram.result }}
RESOLVE_RESULT: ${{ needs.resolve_package.result }}
shell: bash
run: |
set -euo pipefail
failed=0
for item in \
"resolve_package=${RESOLVE_RESULT}" \
"docker_acceptance=${DOCKER_RESULT}" \
"package_telegram=${PACKAGE_TELEGRAM_RESULT}"
do
name="${item%%=*}"
result="${item#*=}"
if [[ "$result" != "success" && "$result" != "skipped" ]]; then
echo "::error::${name} ended with ${result}"
failed=1
fi
done
exit "$failed"

View File

@@ -1,10 +1,18 @@
name: Parity gate
on:
schedule:
- cron: "17 3 * * *"
release:
types: [published]
pull_request:
types: [opened, reopened, synchronize, ready_for_review]
paths:
- "extensions/qa-lab/**"
- "extensions/qa-channel/**"
- "extensions/openai/**"
- "qa/scenarios/**"
- "src/agents/**"
- "src/context-engine/**"
- "src/gateway/**"
- "src/media/**"
- ".github/workflows/parity-gate.yml"
workflow_dispatch:
permissions:
@@ -17,6 +25,7 @@ concurrency:
jobs:
parity-gate:
name: Run the OpenAI / Opus 4.6 parity gate against the qa-lab mock
if: ${{ github.event.pull_request.draft != true }}
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 30
env:
@@ -33,7 +42,7 @@ jobs:
# followthrough gate that expects a fast post-approval read within a 30s
# agent.wait timeout.
QA_PARITY_CONCURRENCY: "1"
OPENCLAW_CI_OPENAI_MODEL: ${{ vars.OPENCLAW_CI_OPENAI_MODEL || 'openai/gpt-5.5' }}
OPENCLAW_CI_OPENAI_MODEL: ${{ vars.OPENCLAW_CI_OPENAI_MODEL }}
OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS: "180000"
OPENAI_API_KEY: ""
ANTHROPIC_API_KEY: ""
@@ -48,11 +57,9 @@ jobs:
steps:
- name: Checkout PR
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1
uses: pnpm/action-setup@v4
- name: Setup Node
uses: actions/setup-node@v6

View File

@@ -35,7 +35,7 @@ jobs:
permissions:
contents: read
outputs:
ref_revision: ${{ steps.ref.outputs.sha }}
ref_sha: ${{ steps.ref.outputs.sha }}
has_candidates: ${{ steps.plan.outputs.has_candidates }}
candidate_count: ${{ steps.plan.outputs.candidate_count }}
skipped_published_count: ${{ steps.plan.outputs.skipped_published_count }}
@@ -44,7 +44,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ github.sha }}
fetch-depth: 0
@@ -151,8 +150,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ needs.preview_plugins_clawhub.outputs.ref_revision }}
ref: ${{ needs.preview_plugins_clawhub.outputs.ref_sha }}
fetch-depth: 1
- name: Setup Node environment
@@ -166,7 +164,6 @@ jobs:
- name: Checkout ClawHub CLI source
uses: actions/checkout@v6
with:
persist-credentials: false
repository: ${{ env.CLAWHUB_REPOSITORY }}
ref: ${{ env.CLAWHUB_REF }}
path: clawhub-source
@@ -190,7 +187,7 @@ jobs:
env:
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
SOURCE_REPO: ${{ github.repository }}
SOURCE_COMMIT: ${{ needs.preview_plugins_clawhub.outputs.ref_revision }}
SOURCE_COMMIT: ${{ needs.preview_plugins_clawhub.outputs.ref_sha }}
SOURCE_REF: ${{ github.ref }}
PACKAGE_TAG: ${{ matrix.plugin.publishTag }}
PACKAGE_DIR: ${{ matrix.plugin.packageDir }}
@@ -212,8 +209,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ needs.preview_plugins_clawhub.outputs.ref_revision }}
ref: ${{ needs.preview_plugins_clawhub.outputs.ref_sha }}
fetch-depth: 1
- name: Setup Node environment
@@ -227,7 +223,6 @@ jobs:
- name: Checkout ClawHub CLI source
uses: actions/checkout@v6
with:
persist-credentials: false
repository: ${{ env.CLAWHUB_REPOSITORY }}
ref: ${{ env.CLAWHUB_REF }}
path: clawhub-source
@@ -271,7 +266,7 @@ jobs:
env:
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
SOURCE_REPO: ${{ github.repository }}
SOURCE_COMMIT: ${{ needs.preview_plugins_clawhub.outputs.ref_revision }}
SOURCE_COMMIT: ${{ needs.preview_plugins_clawhub.outputs.ref_sha }}
SOURCE_REF: ${{ github.ref }}
PACKAGE_TAG: ${{ matrix.plugin.publishTag }}
PACKAGE_DIR: ${{ matrix.plugin.packageDir }}

View File

@@ -46,7 +46,7 @@ jobs:
permissions:
contents: read
outputs:
ref_revision: ${{ steps.ref.outputs.sha }}
ref_sha: ${{ steps.ref.outputs.sha }}
has_candidates: ${{ steps.plan.outputs.has_candidates }}
candidate_count: ${{ steps.plan.outputs.candidate_count }}
matrix: ${{ steps.plan.outputs.matrix }}
@@ -54,7 +54,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
fetch-depth: 0
@@ -152,8 +151,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ needs.preview_plugins_npm.outputs.ref_revision }}
ref: ${{ needs.preview_plugins_npm.outputs.ref_sha }}
fetch-depth: 1
- name: Setup Node environment
@@ -187,8 +185,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ needs.preview_plugins_npm.outputs.ref_revision }}
ref: ${{ needs.preview_plugins_npm.outputs.ref_sha }}
fetch-depth: 1
- name: Setup Node environment

View File

@@ -1,414 +0,0 @@
name: Plugin Prerelease
on:
workflow_dispatch:
inputs:
target_ref:
description: Branch, tag, or full commit SHA to validate
required: false
default: main
type: string
expected_sha:
description: Optional full commit SHA that target_ref must resolve to
required: false
default: ""
type: string
full_release_validation:
description: Enable release-only Docker prerelease lanes from Full Release Validation
required: false
default: false
type: boolean
permissions:
contents: read
concurrency:
group: plugin-prerelease-${{ inputs.target_ref }}
cancel-in-progress: ${{ inputs.target_ref == 'main' }}
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
preflight:
name: Build plugin prerelease plan
runs-on: ubuntu-24.04
timeout-minutes: 15
outputs:
checkout_revision: ${{ steps.manifest.outputs.checkout_revision }}
run_plugin_prerelease_suite: ${{ steps.manifest.outputs.run_plugin_prerelease_suite }}
run_plugin_prerelease_static: ${{ steps.manifest.outputs.run_plugin_prerelease_static }}
plugin_prerelease_static_matrix: ${{ steps.manifest.outputs.plugin_prerelease_static_matrix }}
run_plugin_prerelease_node: ${{ steps.manifest.outputs.run_plugin_prerelease_node }}
plugin_prerelease_node_matrix: ${{ steps.manifest.outputs.plugin_prerelease_node_matrix }}
run_plugin_prerelease_extensions: ${{ steps.manifest.outputs.run_plugin_prerelease_extensions }}
plugin_prerelease_extension_matrix: ${{ steps.manifest.outputs.plugin_prerelease_extension_matrix }}
run_plugin_prerelease_docker: ${{ steps.manifest.outputs.run_plugin_prerelease_docker }}
plugin_prerelease_docker_lanes: ${{ steps.manifest.outputs.plugin_prerelease_docker_lanes }}
steps:
- name: Checkout target
uses: actions/checkout@v6
with:
ref: ${{ inputs.target_ref }}
fetch-depth: 1
fetch-tags: false
persist-credentials: false
submodules: false
- name: Build plugin prerelease manifest
id: manifest
env:
EXPECTED_SHA: ${{ inputs.expected_sha }}
FULL_RELEASE_VALIDATION: ${{ inputs.full_release_validation && 'true' || 'false' }}
run: |
node --input-type=module <<'EOF'
import { appendFileSync } from "node:fs";
import { execFileSync } from "node:child_process";
const createMatrix = (include) => ({ include });
const outputPath = process.env.GITHUB_OUTPUT;
const checkoutRevision = execFileSync("git", ["rev-parse", "HEAD"], {
encoding: "utf8",
}).trim();
const expectedSha = (process.env.EXPECTED_SHA ?? "").trim();
const fullReleaseValidation = process.env.FULL_RELEASE_VALIDATION === "true";
if (expectedSha && expectedSha !== checkoutRevision) {
console.error(
`target_ref resolved to ${checkoutRevision}, expected ${expectedSha}`,
);
process.exit(1);
}
let pluginPrereleasePlan = { staticChecks: [], dockerLanes: [] };
let extensionShards = [];
let nodeShards = [];
try {
const { assertPluginPrereleaseTestPlanComplete } = await import(
"./scripts/lib/plugin-prerelease-test-plan.mjs"
);
pluginPrereleasePlan = assertPluginPrereleaseTestPlanComplete();
} catch (error) {
const errorCode =
error && typeof error === "object" && "code" in error ? error.code : "";
const moduleUrl =
error && typeof error === "object" && "url" in error ? String(error.url) : "";
if (
errorCode === "ERR_MODULE_NOT_FOUND" &&
moduleUrl.endsWith("/scripts/lib/plugin-prerelease-test-plan.mjs")
) {
console.warn(
"Plugin prerelease plan unavailable in target ref; skipping static and Docker plugin prerelease lanes.",
);
} else {
throw error;
}
}
try {
const { createExtensionTestShards, DEFAULT_EXTENSION_TEST_SHARD_COUNT } = await import(
"./scripts/lib/extension-test-plan.mjs"
);
extensionShards = createExtensionTestShards({
shardCount: DEFAULT_EXTENSION_TEST_SHARD_COUNT,
}).map((shard) => ({
check_name: shard.checkName,
extensions_csv: shard.extensionIds.join(","),
runner: [0, 1, 2, 3].includes(shard.index)
? "blacksmith-8vcpu-ubuntu-2404"
: "blacksmith-4vcpu-ubuntu-2404",
shard_index: shard.index + 1,
task: "extensions-batch",
}));
} catch (error) {
const errorCode =
error && typeof error === "object" && "code" in error ? error.code : "";
const moduleUrl =
error && typeof error === "object" && "url" in error ? String(error.url) : "";
if (
errorCode === "ERR_MODULE_NOT_FOUND" &&
moduleUrl.endsWith("/scripts/lib/extension-test-plan.mjs")
) {
console.warn(
"Extension test plan unavailable in target ref; skipping extension prerelease shards.",
);
} else {
throw error;
}
}
try {
const { createNodeTestShards } = await import("./scripts/lib/ci-node-test-plan.mjs");
nodeShards = createNodeTestShards({
includeReleaseOnlyPluginShards: true,
})
.filter((shard) => shard.shardName === "agentic-plugins")
.map((shard) => ({
check_name: shard.checkName,
runtime: "node",
task: "test-shard",
shard_name: shard.shardName,
configs: shard.configs,
includePatterns: shard.includePatterns,
runner: shard.runner,
}));
} catch (error) {
const errorCode =
error && typeof error === "object" && "code" in error ? error.code : "";
const moduleUrl =
error && typeof error === "object" && "url" in error ? String(error.url) : "";
if (
errorCode === "ERR_MODULE_NOT_FOUND" &&
moduleUrl.endsWith("/scripts/lib/ci-node-test-plan.mjs")
) {
console.warn(
"Node test plan unavailable in target ref; skipping release-only plugin Node shard.",
);
} else {
throw error;
}
}
const staticChecks = pluginPrereleasePlan.staticChecks.map((check) => ({
check_name: check.checkName,
command: check.command,
task: check.check,
}));
const dockerLanes = pluginPrereleasePlan.dockerLanes;
const runStatic = staticChecks.length > 0;
const runNode = nodeShards.length > 0;
const runExtensions = extensionShards.length > 0;
const runDocker = fullReleaseValidation && dockerLanes.length > 0;
const runSuite = runStatic || runNode || runExtensions || runDocker;
const manifest = {
checkout_revision: checkoutRevision,
run_plugin_prerelease_suite: runSuite,
run_plugin_prerelease_static: runStatic,
plugin_prerelease_static_matrix: createMatrix(staticChecks),
run_plugin_prerelease_node: runNode,
plugin_prerelease_node_matrix: createMatrix(nodeShards),
run_plugin_prerelease_extensions: runExtensions,
plugin_prerelease_extension_matrix: createMatrix(extensionShards),
run_plugin_prerelease_docker: runDocker,
plugin_prerelease_docker_lanes: dockerLanes.join(" "),
};
for (const [key, value] of Object.entries(manifest)) {
appendFileSync(
outputPath,
`${key}=${typeof value === "string" ? value : JSON.stringify(value)}\n`,
"utf8",
);
}
EOF
plugin-prerelease-static-shard:
permissions:
contents: read
name: ${{ matrix.check_name }}
needs: [preflight]
if: needs.preflight.outputs.run_plugin_prerelease_static == 'true'
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 45
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.preflight.outputs.plugin_prerelease_static_matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.checkout_revision }}
fetch-depth: 1
fetch-tags: false
persist-credentials: false
submodules: false
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
- name: Run plugin prerelease static shard
env:
PLUGIN_PRERELEASE_COMMAND: ${{ matrix.command }}
PLUGIN_PRERELEASE_TASK: ${{ matrix.task }}
shell: bash
run: |
set -euo pipefail
echo "Running ${PLUGIN_PRERELEASE_TASK}: ${PLUGIN_PRERELEASE_COMMAND}"
bash -c "$PLUGIN_PRERELEASE_COMMAND"
plugin-prerelease-node-shard:
permissions:
contents: read
name: ${{ matrix.check_name }}
needs: [preflight]
if: needs.preflight.outputs.run_plugin_prerelease_node == 'true'
runs-on: ${{ matrix.runner || 'ubuntu-24.04' }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.preflight.outputs.plugin_prerelease_node_matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.checkout_revision }}
fetch-depth: 1
fetch-tags: false
persist-credentials: false
submodules: false
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
- name: Configure Node test resources
run: echo "OPENCLAW_VITEST_MAX_WORKERS=2" >> "$GITHUB_ENV"
- name: Run release-only plugin Node shard
env:
NODE_OPTIONS: --max-old-space-size=6144
OPENCLAW_NODE_TEST_CONFIGS_JSON: ${{ toJson(matrix.configs) }}
OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON: ${{ toJson(matrix.includePatterns) }}
OPENCLAW_VITEST_SHARD_NAME: ${{ matrix.shard_name }}
OPENCLAW_TEST_PROJECTS_PARALLEL: "2"
shell: bash
run: |
set -euo pipefail
node --input-type=module <<'EOF'
import { spawnSync } from "node:child_process";
import { writeFileSync } from "node:fs";
import { join } from "node:path";
const configs = JSON.parse(process.env.OPENCLAW_NODE_TEST_CONFIGS_JSON ?? "[]");
if (!Array.isArray(configs) || configs.length === 0) {
console.error("Missing node test shard configs");
process.exit(1);
}
const includePatterns = JSON.parse(
process.env.OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON ?? "null",
);
const childEnv = { ...process.env };
if (Array.isArray(includePatterns) && includePatterns.length > 0) {
const includeFile = join(
process.env.RUNNER_TEMP ?? ".",
`node-test-include-${process.env.GITHUB_JOB ?? "local"}-${Date.now()}.json`,
);
writeFileSync(includeFile, JSON.stringify(includePatterns), "utf8");
childEnv.OPENCLAW_VITEST_INCLUDE_FILE = includeFile;
}
const result = spawnSync(
"pnpm",
["exec", "node", "scripts/test-projects.mjs", ...configs],
{
env: childEnv,
stdio: "inherit",
},
);
process.exit(result.status ?? 1);
EOF
plugin-prerelease-extension-shard:
permissions:
contents: read
name: ${{ matrix.check_name }}
needs: [preflight]
if: needs.preflight.outputs.run_plugin_prerelease_extensions == 'true'
runs-on: ${{ matrix.runner }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.preflight.outputs.plugin_prerelease_extension_matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.checkout_revision }}
fetch-depth: 1
fetch-tags: false
persist-credentials: false
submodules: false
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
- name: Run extension shard
env:
NODE_OPTIONS: --max-old-space-size=6144
OPENCLAW_EXTENSION_BATCH_PARALLEL: 2
OPENCLAW_VITEST_MAX_WORKERS: 1
OPENCLAW_EXTENSION_BATCH: ${{ matrix.extensions_csv }}
run: pnpm test:extensions:batch -- "$OPENCLAW_EXTENSION_BATCH"
plugin-prerelease-docker-suite:
name: plugin-prerelease-docker-suite
needs: [preflight]
if: ${{ inputs.full_release_validation && needs.preflight.outputs.run_plugin_prerelease_docker == 'true' }}
permissions:
actions: read
contents: read
packages: write
pull-requests: read
uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml
with:
ref: ${{ needs.preflight.outputs.checkout_revision }}
include_repo_e2e: false
include_release_path_suites: false
include_openwebui: false
docker_lanes: ${{ needs.preflight.outputs.plugin_prerelease_docker_lanes }}
targeted_docker_lane_group_size: 4
include_live_suites: false
live_models_only: false
plugin-prerelease-suite:
permissions:
contents: read
name: plugin-prerelease-suite
needs:
- preflight
- plugin-prerelease-static-shard
- plugin-prerelease-node-shard
- plugin-prerelease-extension-shard
- plugin-prerelease-docker-suite
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_plugin_prerelease_suite == 'true' }}
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Verify plugin prerelease suite
env:
RUN_STATIC: ${{ needs.preflight.outputs.run_plugin_prerelease_static }}
RUN_NODE: ${{ needs.preflight.outputs.run_plugin_prerelease_node }}
RUN_EXTENSIONS: ${{ needs.preflight.outputs.run_plugin_prerelease_extensions }}
RUN_DOCKER: ${{ needs.preflight.outputs.run_plugin_prerelease_docker }}
STATIC_RESULT: ${{ needs.plugin-prerelease-static-shard.result }}
NODE_RESULT: ${{ needs.plugin-prerelease-node-shard.result }}
EXTENSIONS_RESULT: ${{ needs.plugin-prerelease-extension-shard.result }}
DOCKER_RESULT: ${{ needs.plugin-prerelease-docker-suite.result }}
shell: bash
run: |
set -euo pipefail
failed=0
check_required() {
local name="$1"
local required="$2"
local status="$3"
if [ "$required" != "true" ]; then
return 0
fi
if [ "$status" != "success" ]; then
echo "::error::${name} ended with ${status}"
failed=1
fi
}
check_required "plugin-prerelease-static" "$RUN_STATIC" "$STATIC_RESULT"
check_required "plugin-prerelease-node" "$RUN_NODE" "$NODE_RESULT"
check_required "plugin-prerelease-extensions" "$RUN_EXTENSIONS" "$EXTENSIONS_RESULT"
check_required "plugin-prerelease-docker" "$RUN_DOCKER" "$DOCKER_RESULT"
exit "$failed"

View File

@@ -18,19 +18,6 @@ on:
description: Optional comma-separated Discord scenario ids
required: false
type: string
matrix_profile:
description: Matrix QA profile for the live Matrix lane
required: false
default: all
type: choice
options:
- fast
- all
- transport
- media
- e2ee-smoke
- e2ee-deep
- e2ee-cli
permissions:
contents: read
@@ -44,7 +31,7 @@ env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "10.33.0"
OPENCLAW_CI_OPENAI_MODEL: ${{ vars.OPENCLAW_CI_OPENAI_MODEL || 'openai/gpt-5.5' }}
OPENCLAW_CI_OPENAI_MODEL: ${{ vars.OPENCLAW_CI_OPENAI_MODEL }}
OPENCLAW_BUILD_PRIVATE_QA: "1"
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
@@ -81,13 +68,12 @@ jobs:
needs: authorize_actor
runs-on: blacksmith-8vcpu-ubuntu-2404
outputs:
selected_revision: ${{ steps.validate.outputs.selected_revision }}
selected_sha: ${{ steps.validate.outputs.selected_sha }}
trusted_reason: ${{ steps.validate.outputs.trusted_reason }}
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
fetch-depth: 0
@@ -99,27 +85,27 @@ jobs:
shell: bash
run: |
set -euo pipefail
selected_revision="$(git rev-parse HEAD)"
selected_sha="$(git rev-parse HEAD)"
trusted_reason=""
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
if git merge-base --is-ancestor "$selected_revision" refs/remotes/origin/main; then
if git merge-base --is-ancestor "$selected_sha" refs/remotes/origin/main; then
trusted_reason="main-ancestor"
elif git tag --points-at "$selected_revision" | grep -Eq '^v'; then
elif git tag --points-at "$selected_sha" | grep -Eq '^v'; then
trusted_reason="release-tag"
elif [[ "$INPUT_REF" =~ ^release/[0-9]{4}\.[0-9]+\.[0-9]+$ ]]; then
git fetch --no-tags origin "+refs/heads/${INPUT_REF}:refs/remotes/origin/${INPUT_REF}"
release_branch_sha="$(git rev-parse "refs/remotes/origin/${INPUT_REF}")"
if [[ "$selected_revision" == "$release_branch_sha" ]]; then
if [[ "$selected_sha" == "$release_branch_sha" ]]; then
trusted_reason="release-branch-head"
fi
else
pr_head_count="$(
gh api \
-H "Accept: application/vnd.github+json" \
"repos/${GITHUB_REPOSITORY}/commits/${selected_revision}/pulls" \
--jq '[.[] | select(.state == "open" and .head.repo.full_name == "'"${GITHUB_REPOSITORY}"'" and .head.sha == "'"${selected_revision}"'")] | length'
"repos/${GITHUB_REPOSITORY}/commits/${selected_sha}/pulls" \
--jq '[.[] | select(.state == "open" and .head.repo.full_name == "'"${GITHUB_REPOSITORY}"'" and .head.sha == "'"${selected_sha}"'")] | length'
)"
if [[ "$pr_head_count" != "0" ]]; then
trusted_reason="open-pr-head"
@@ -127,23 +113,23 @@ jobs:
fi
if [[ -z "$trusted_reason" ]]; then
echo "Ref '${INPUT_REF}' resolved to $selected_revision, which is not trusted for this secret-bearing QA run." >&2
echo "Ref '${INPUT_REF}' resolved to $selected_sha, which is not trusted for this secret-bearing QA run." >&2
echo "Allowed refs must be on main, point to a release tag, match a release branch head, or match an open PR head in ${GITHUB_REPOSITORY}." >&2
exit 1
fi
echo "selected_revision=$selected_revision" >> "$GITHUB_OUTPUT"
echo "selected_sha=$selected_sha" >> "$GITHUB_OUTPUT"
echo "trusted_reason=$trusted_reason" >> "$GITHUB_OUTPUT"
{
echo "Validated ref: \`${INPUT_REF}\`"
echo "Resolved SHA: \`$selected_revision\`"
echo "Resolved SHA: \`$selected_sha\`"
echo "Trust reason: \`$trusted_reason\`"
} >> "$GITHUB_STEP_SUMMARY"
run_mock_parity:
name: Run QA Lab parity gate
needs: [validate_selected_ref]
runs-on: blacksmith-8vcpu-ubuntu-2404
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 30
env:
QA_PARITY_CONCURRENCY: "1"
@@ -158,8 +144,7 @@ jobs:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ needs.validate_selected_ref.outputs.selected_revision }}
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
fetch-depth: 1
- name: Setup Node environment
@@ -214,16 +199,14 @@ jobs:
run_live_matrix:
name: Run Matrix live QA lane
needs: [authorize_actor, validate_selected_ref]
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.matrix_profile == 'all') }}
runs-on: blacksmith-8vcpu-ubuntu-2404
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 60
environment: qa-live-shared
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ needs.validate_selected_ref.outputs.selected_revision }}
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
fetch-depth: 1
- name: Setup Node environment
@@ -253,29 +236,20 @@ jobs:
shell: bash
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
INPUT_MATRIX_PROFILE: ${{ github.event_name == 'workflow_dispatch' && inputs.matrix_profile || 'fast' }}
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
OPENCLAW_QA_MATRIX_NO_REPLY_WINDOW_MS: "3000"
run: |
set -euo pipefail
output_dir=".artifacts/qa-e2e/matrix-live-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
echo "output_dir=${output_dir}" >> "$GITHUB_OUTPUT"
matrix_args=(
pnpm openclaw qa matrix \
--repo-root . \
--output-dir "${output_dir}" \
--provider-mode live-frontier \
--model "${OPENCLAW_CI_OPENAI_MODEL}" \
--alt-model "${OPENCLAW_CI_OPENAI_MODEL}" \
--profile "${INPUT_MATRIX_PROFILE}" \
--fast
)
if pnpm openclaw qa matrix --help 2>/dev/null | grep -F -q -- "--fail-fast"; then
matrix_args+=(--fail-fast)
fi
pnpm openclaw qa matrix "${matrix_args[@]}"
- name: Upload Matrix QA artifacts
if: always()
@@ -286,101 +260,17 @@ jobs:
retention-days: 14
if-no-files-found: warn
run_live_matrix_sharded:
name: Run Matrix live QA lane (${{ matrix.profile }})
needs: [authorize_actor, validate_selected_ref]
if: ${{ github.event_name == 'workflow_dispatch' && inputs.matrix_profile == 'all' }}
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 60
environment: qa-live-shared
strategy:
fail-fast: false
matrix:
profile:
- transport
- media
- e2ee-smoke
- e2ee-deep
- e2ee-cli
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ needs.validate_selected_ref.outputs.selected_revision }}
fetch-depth: 1
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Validate required QA credential env
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
shell: bash
run: |
set -euo pipefail
if [[ -z "${OPENAI_API_KEY:-}" ]]; then
echo "Missing required OPENAI_API_KEY." >&2
exit 1
fi
- name: Build private QA runtime
run: pnpm build
- name: Run Matrix live lane shard
id: run_lane
shell: bash
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
OPENCLAW_QA_MATRIX_NO_REPLY_WINDOW_MS: "3000"
run: |
set -euo pipefail
output_dir=".artifacts/qa-e2e/matrix-live-${{ matrix.profile }}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
echo "output_dir=${output_dir}" >> "$GITHUB_OUTPUT"
matrix_args=(
--repo-root . \
--output-dir "${output_dir}" \
--provider-mode live-frontier \
--model "${OPENCLAW_CI_OPENAI_MODEL}" \
--alt-model "${OPENCLAW_CI_OPENAI_MODEL}" \
--profile "${{ matrix.profile }}" \
--fast
)
if pnpm openclaw qa matrix --help 2>/dev/null | grep -F -q -- "--fail-fast"; then
matrix_args+=(--fail-fast)
fi
pnpm openclaw qa matrix "${matrix_args[@]}"
- name: Upload Matrix QA shard artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: qa-live-matrix-${{ matrix.profile }}-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.run_lane.outputs.output_dir }}
retention-days: 14
if-no-files-found: warn
run_live_telegram:
name: Run Telegram live QA lane with Convex leases
needs: [authorize_actor, validate_selected_ref]
runs-on: blacksmith-8vcpu-ubuntu-2404
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 60
environment: qa-live-shared
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ needs.validate_selected_ref.outputs.selected_revision }}
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
fetch-depth: 1
- name: Setup Node environment
@@ -465,15 +355,14 @@ jobs:
run_live_discord:
name: Run Discord live QA lane with Convex leases
needs: [authorize_actor, validate_selected_ref]
runs-on: blacksmith-8vcpu-ubuntu-2404
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 60
environment: qa-live-shared
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ needs.validate_selected_ref.outputs.selected_revision }}
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
fetch-depth: 1
- name: Setup Node environment

View File

@@ -4,32 +4,6 @@ on:
schedule:
- cron: "17 3 * * *"
workflow_dispatch:
inputs:
backfill_stale_closures:
description: "Close currently stale-eligible issues and PRs with the Barnacle app"
required: false
type: boolean
default: false
dry_run:
description: "List matching stale-eligible items without closing them"
required: false
type: boolean
default: true
include_issues:
description: "Include stale-eligible issues in the backfill"
required: false
type: boolean
default: true
include_prs:
description: "Include stale-eligible pull requests in the backfill"
required: false
type: boolean
default: true
max_closures:
description: "Maximum items to close when dry_run is false"
required: false
type: number
default: 50
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
@@ -38,7 +12,6 @@ permissions: {}
jobs:
stale:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.backfill_stale_closures != true }}
permissions:
issues: write
pull-requests: write
@@ -62,13 +35,13 @@ jobs:
uses: actions/stale@v10
with:
repo-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
days-before-issue-stale: 14
days-before-issue-close: 7
days-before-pr-stale: 14
days-before-pr-close: 7
days-before-issue-stale: 7
days-before-issue-close: 5
days-before-pr-stale: 5
days-before-pr-close: 3
stale-issue-label: stale
stale-pr-label: stale
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale
exempt-pr-labels: maintainer,no-stale,bad-barnacle
operations-per-run: 2000
ascending: true
@@ -87,7 +60,7 @@ jobs:
close-issue-reason: not_planned
close-pr-message: |
Closing due to inactivity.
If you believe this PR should be revived, post in #clawtributors on Discord to talk to a maintainer.
If you believe this PR should be revived, post in #pr-thunderdome-dangerzone on Discord to talk to a maintainer.
That channel is the escape hatch for high-quality PRs that get auto-closed.
- name: Mark stale assigned issues (primary)
id: assigned-issue-stale-primary
@@ -100,7 +73,7 @@ jobs:
days-before-pr-stale: -1
days-before-pr-close: -1
stale-issue-label: stale
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale
operations-per-run: 2000
ascending: true
include-only-assigned: true
@@ -122,7 +95,7 @@ jobs:
days-before-issue-stale: -1
days-before-issue-close: -1
days-before-pr-stale: 27
days-before-pr-close: 7
days-before-pr-close: 3
stale-pr-label: stale
exempt-pr-labels: maintainer,no-stale,bad-barnacle
operations-per-run: 2000
@@ -135,7 +108,7 @@ jobs:
Please add updates or it will be closed.
close-pr-message: |
Closing due to inactivity.
If you believe this PR should be revived, post in #clawtributors on Discord to talk to a maintainer.
If you believe this PR should be revived, post in #pr-thunderdome-dangerzone on Discord to talk to a maintainer.
That channel is the escape hatch for high-quality PRs that get auto-closed.
- name: Check stale state cache
id: stale-state
@@ -166,13 +139,13 @@ jobs:
uses: actions/stale@v10
with:
repo-token: ${{ steps.app-token-fallback.outputs.token }}
days-before-issue-stale: 14
days-before-issue-close: 7
days-before-pr-stale: 14
days-before-pr-close: 7
days-before-issue-stale: 7
days-before-issue-close: 5
days-before-pr-stale: 5
days-before-pr-close: 3
stale-issue-label: stale
stale-pr-label: stale
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale
exempt-pr-labels: maintainer,no-stale,bad-barnacle
operations-per-run: 2000
ascending: true
@@ -191,7 +164,7 @@ jobs:
close-issue-reason: not_planned
close-pr-message: |
Closing due to inactivity.
If you believe this PR should be revived, post in #clawtributors on Discord to talk to a maintainer.
If you believe this PR should be revived, post in #pr-thunderdome-dangerzone on Discord to talk to a maintainer.
That channel is the escape hatch for high-quality PRs that get auto-closed.
- name: Mark stale assigned issues (fallback)
if: (steps.assigned-issue-stale-primary.outcome == 'failure' || steps.stale-state.outputs.has_state == 'true') && steps.app-token-fallback.outputs.token != ''
@@ -203,7 +176,7 @@ jobs:
days-before-pr-stale: -1
days-before-pr-close: -1
stale-issue-label: stale
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale
operations-per-run: 2000
ascending: true
include-only-assigned: true
@@ -224,7 +197,7 @@ jobs:
days-before-issue-stale: -1
days-before-issue-close: -1
days-before-pr-stale: 27
days-before-pr-close: 7
days-before-pr-close: 3
stale-pr-label: stale
exempt-pr-labels: maintainer,no-stale,bad-barnacle
operations-per-run: 2000
@@ -237,256 +210,10 @@ jobs:
Please add updates or it will be closed.
close-pr-message: |
Closing due to inactivity.
If you believe this PR should be revived, post in #clawtributors on Discord to talk to a maintainer.
If you believe this PR should be revived, post in #pr-thunderdome-dangerzone on Discord to talk to a maintainer.
That channel is the escape hatch for high-quality PRs that get auto-closed.
backfill-stale-closures:
if: ${{ github.event_name == 'workflow_dispatch' && inputs.backfill_stale_closures == true }}
permissions:
issues: write
pull-requests: write
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
- uses: actions/create-github-app-token@v3
id: app-token
with:
app-id: "2971289"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }}
- name: Backfill stale closures
uses: actions/github-script@v9
env:
DRY_RUN: ${{ inputs.dry_run }}
INCLUDE_ISSUES: ${{ inputs.include_issues }}
INCLUDE_PRS: ${{ inputs.include_prs }}
MAX_CLOSURES: ${{ inputs.max_closures }}
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const dayMs = 24 * 60 * 60 * 1000;
const dryRun = process.env.DRY_RUN !== "false";
const includeIssues = process.env.INCLUDE_ISSUES !== "false";
const includePrs = process.env.INCLUDE_PRS !== "false";
const maxClosures = Math.max(0, Number(process.env.MAX_CLOSURES || "50"));
const nowMs = Date.now();
const { owner, repo } = context.repo;
const issueExemptLabels = new Set([
"enhancement",
"maintainer",
"pinned",
"security",
"no-stale",
"bad-barnacle",
]);
const prExemptLabels = new Set(["maintainer", "no-stale", "bad-barnacle"]);
const maintainerAssociations = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
const maintainerLogins = new Set([
"altaywtf",
"BunsDev",
"cpojer",
"gumadeiras",
"hydro13",
"hxy91819",
"jalehman",
"joshavant",
"joshp123",
"mbelinky",
"mukhtharcm",
"ngutman",
"obviyus",
"odysseus0",
"onutc",
"osolmaz",
"sebslight",
"sliverp",
"steipete",
"thewilloftheshadow",
"tyler6204",
"velvet-shark",
"vignesh07",
"vincentkoc",
"visionik",
].map(login => login.toLowerCase()));
const issueCloseMessage = [
"Closing due to inactivity.",
"If this is still an issue, please retry on the latest OpenClaw release and share updated details.",
"If you are absolutely sure it still happens on the latest release, open a new issue with fresh steps to reproduce.",
].join("\n");
const prCloseMessage = [
"Closing due to inactivity.",
"If you believe this PR should be revived, post in #clawtributors on Discord to talk to a maintainer.",
"That channel is the escape hatch for high-quality PRs that get auto-closed.",
].join("\n");
const hasAny = (labels, exemptLabels) => {
for (const label of labels) {
if (exemptLabels.has(label)) {
return true;
}
}
return false;
};
const isOlderThan = (dateString, days) => {
const timestamp = Date.parse(dateString);
return Number.isFinite(timestamp) && timestamp < nowMs - days * dayMs;
};
const candidates = [];
const skipped = {
missingStale: 0,
exemptLabel: 0,
maintainerAuthor: 0,
maintainerAssignee: 0,
notOldEnough: 0,
disabledType: 0,
};
for await (const response of github.paginate.iterator(github.rest.issues.listForRepo, {
owner,
repo,
state: "open",
sort: "updated",
direction: "asc",
per_page: 100,
})) {
for (const item of response.data) {
const isPr = Boolean(item.pull_request);
if ((isPr && !includePrs) || (!isPr && !includeIssues)) {
skipped.disabledType += 1;
continue;
}
const labels = new Set((item.labels || []).map(label => label.name));
if (!labels.has("stale")) {
skipped.missingStale += 1;
continue;
}
const exemptLabels = isPr ? prExemptLabels : issueExemptLabels;
if (hasAny(labels, exemptLabels)) {
skipped.exemptLabel += 1;
continue;
}
if (maintainerAssociations.has(item.author_association)) {
skipped.maintainerAuthor += 1;
continue;
}
const assigned = (item.assignees || []).length > 0;
const assignedToMaintainer = (item.assignees || []).some(assignee =>
maintainerLogins.has(assignee.login.toLowerCase()),
);
if (assignedToMaintainer) {
skipped.maintainerAssignee += 1;
continue;
}
let eligible = false;
let lane = "";
if (isPr && assigned) {
lane = "assigned-pr";
eligible = isOlderThan(item.created_at, 34) && isOlderThan(item.updated_at, 7);
} else if (isPr) {
lane = "unassigned-pr";
eligible = isOlderThan(item.updated_at, 7);
} else if (assigned) {
lane = "assigned-issue";
eligible = isOlderThan(item.updated_at, 10);
} else {
lane = "unassigned-issue";
eligible = isOlderThan(item.updated_at, 7);
}
if (!eligible) {
skipped.notOldEnough += 1;
continue;
}
candidates.push({
number: item.number,
title: item.title,
lane,
isPr,
assigned,
createdAt: item.created_at,
updatedAt: item.updated_at,
authorAssociation: item.author_association,
url: item.html_url,
});
}
}
const countsByLane = candidates.reduce((counts, candidate) => {
counts[candidate.lane] = (counts[candidate.lane] || 0) + 1;
return counts;
}, {});
const selected = candidates.slice(0, maxClosures);
core.info(`Dry run: ${dryRun}`);
core.info(`Candidates: ${candidates.length}`);
core.info(`Selected: ${selected.length}`);
core.info(`Counts by lane: ${JSON.stringify(countsByLane)}`);
core.info(`Skipped: ${JSON.stringify(skipped)}`);
for (const candidate of selected) {
core.info(`${dryRun ? "Would close" : "Closing"} ${candidate.lane} #${candidate.number}: ${candidate.title} (${candidate.url})`);
}
await core.summary
.addHeading("Stale Closure Backfill")
.addRaw(`Dry run: ${dryRun}\n\n`)
.addRaw(`Candidates: ${candidates.length}\n\n`)
.addRaw(`Selected: ${selected.length}\n\n`)
.addCodeBlock(JSON.stringify({ countsByLane, skipped }, null, 2), "json")
.addTable([
[
{ data: "Lane", header: true },
{ data: "Number", header: true },
{ data: "Title", header: true },
{ data: "URL", header: true },
],
...selected.map(candidate => [
candidate.lane,
String(candidate.number),
candidate.title,
candidate.url,
]),
])
.write();
if (dryRun) {
return;
}
for (const candidate of selected) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: candidate.number,
body: candidate.isPr ? prCloseMessage : issueCloseMessage,
});
if (candidate.isPr) {
await github.rest.pulls.update({
owner,
repo,
pull_number: candidate.number,
state: "closed",
});
} else {
await github.rest.issues.update({
owner,
repo,
issue_number: candidate.number,
state: "closed",
state_reason: "not_planned",
});
}
}
lock-closed-issues:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.backfill_stale_closures != true }}
permissions:
issues: write
runs-on: blacksmith-16vcpu-ubuntu-2404

View File

@@ -129,7 +129,7 @@ jobs:
- name: Run Codex test performance agent
if: steps.gate.outputs.run_agent == 'true'
uses: openai/codex-action@5c3f4ccdb2b8790f73d6b21751ac00e602aa0c02
uses: openai/codex-action@v1
with:
openai-api-key: ${{ secrets.OPENCLAW_TEST_PERF_AGENT_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
prompt-file: .github/codex/prompts/test-performance-agent.md

12
.gitignore vendored
View File

@@ -3,10 +3,8 @@ node_modules
.env
docker-compose.override.yml
docker-compose.extra.yml
docker-compose.sandbox.yml
dist
dist-runtime/
dist-sea/
pnpm-lock.yaml
bun.lock
bun.lockb
@@ -104,10 +102,6 @@ USER.md
.agents/skills/*
!.agents/skills/blacksmith-testbox/
!.agents/skills/blacksmith-testbox/**
!.agents/skills/crabbox/
!.agents/skills/crabbox/**
!.agents/skills/gitcrawl/
!.agents/skills/gitcrawl/**
!.agents/skills/openclaw-ghsa-maintainer/
!.agents/skills/openclaw-ghsa-maintainer/**
!.agents/skills/openclaw-parallels-smoke/
@@ -140,7 +134,6 @@ USER.md
.agent/*.json
!.agent/workflows/
/local/
/client_secret_*.json
package-lock.json
.claude/
.agent/
@@ -190,13 +183,8 @@ changelog/fragments/
test/fixtures/openclaw-vitest-unit-report.json
analysis/
.artifacts/qa-e2e/
/runs/
/data/rtt.jsonl
extensions/qa-lab/web/dist/
# Generated bundled plugin runtime dependency manifests
extensions/**/.openclaw-runtime-deps.json
extensions/**/.openclaw-runtime-deps-stamp.json
# Output dir for scripts/run-opengrep.sh (local opengrep scans)
/.opengrep-out/

View File

@@ -25,7 +25,6 @@
"eslint/no-sequences": "error",
"eslint/no-self-compare": "error",
"eslint/no-shadow": "off",
"eslint/no-underscore-dangle": "off",
"eslint/no-var": "error",
"eslint/no-useless-call": "error",
"eslint/no-useless-computed-key": "error",

View File

@@ -1,95 +0,0 @@
# .semgrepignore — single source of truth for paths excluded from
# opengrep / semgrep scans run against this repo.
#
# Syntax: gitignore-style globs (https://git-scm.com/docs/gitignore).
# Consumed automatically by `opengrep scan` and `semgrep scan`. The compiled
# detector rulepacks under security/opengrep/ and the GitHub Actions workflows
# under .github/workflows/opengrep-*.yml all rely on this file rather than
# duplicating exclude lists in 50+ places.
#
# When adding a new test naming convention, fixture directory, or QA-tooling
# extension to the codebase, add its glob here so the security rulepacks
# stop firing on it. Real product code should never match anything in this
# file.
# ----------------------------------------------------------------------------
# Standard test file suffixes
# ----------------------------------------------------------------------------
*.test.*
*.spec.*
# ----------------------------------------------------------------------------
# Fixture & mock file suffixes (cover both .foo and -foo styles used in repo)
# ----------------------------------------------------------------------------
*.fixture.*
*-fixture.*
*-fixtures.*
*.mock.*
*-mock.*
*-mocks.*
# ----------------------------------------------------------------------------
# Test helper / harness / support / shared / utils naming conventions
# ----------------------------------------------------------------------------
*.test-helper.*
*.test-helpers.*
*-test-helpers.*
*.test-harness.*
*-test-harness.*
*.test-support.*
*-test-support.*
*.test-shared.*
*-test-shared.*
*.test-mocks.*
*-test-mocks.*
*.test-utils.*
*-test-utils.*
*.test-fixtures.*
*-test-fixtures.*
*.e2e-test-helpers.*
# Bare top-of-dir test helper files (e.g. extensions/foo/src/test-helpers.ts)
test-helper.*
test-helpers.*
test-harness.*
test-support.*
test-shared.*
test-utils.*
test-mocks.*
test-fixtures.*
test-fetch.*
test-manager-helpers.*
# ----------------------------------------------------------------------------
# Test / mock / fixture directories anywhere in the tree
# ----------------------------------------------------------------------------
__tests__/
__mocks__/
test/
tests/
test-fixtures/
test-fixture/
test-helpers/
test-utils/
test-support/
test-mocks/
test-harness/
fixtures/
mocks/
# ----------------------------------------------------------------------------
# QA tooling — entire QA-only directories and extensions, not product code
# ----------------------------------------------------------------------------
qa/
qa-lab/
extensions/qa-*/
# ----------------------------------------------------------------------------
# Top-level scripts that drive tests rather than ship product behavior
# ----------------------------------------------------------------------------
scripts/test-*
scripts/run-vitest*
scripts/run-tests*
scripts/lib/test-*
scripts/lib/extension-test-*
scripts/lib/vitest-*

View File

@@ -50,15 +50,11 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
- Prod sweep: `pnpm check`; tests: `pnpm test`, `pnpm test:changed`, `pnpm test:serial`, `pnpm test:coverage`.
- Extension tests: `pnpm test:extensions`, `pnpm test extensions`, `pnpm test extensions/<id>`.
- Targeted tests: `pnpm test <path-or-filter> [vitest args...]`; never raw `vitest`.
- Vitest flags only; no Jest flags like `--runInBand`. For serial runs use `pnpm test:serial` or `OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test ...`.
- Typecheck: `tsgo` lanes only (`pnpm tsgo*`, `pnpm check:test-types`); do not add `tsc --noEmit`, `typecheck`, `check:types`.
- Formatting: use `oxfmt`, not Prettier. Prefer `pnpm format:check` / `pnpm format`; for targeted files use `pnpm exec oxfmt --check --threads=1 <files...>` or `pnpm exec oxfmt --write --threads=1 <files...>`.
- Linting: use repo wrappers (`pnpm lint:*`, `scripts/run-oxlint.mjs`); do not invoke generic JS formatters/lints unless a repo script uses them.
- Heavy checks: `OPENCLAW_LOCAL_CHECK=1`, mode `OPENCLAW_LOCAL_CHECK_MODE=throttled|full`; CI/shared use `OPENCLAW_LOCAL_CHECK=0`.
- Blacksmith/Testbox: on maintainer machines with Blacksmith access, broad/shared validation defaults to Testbox. This includes `pnpm check`, `pnpm check:changed`, `pnpm test`, `pnpm test:changed`, Docker/E2E/live/package/build gates, and any command likely to fan out across many Vitest projects. Do not start those broad gates locally unless the user explicitly asks for local proof or sets `OPENCLAW_LOCAL_CHECK_MODE=throttled|full`.
- Local validation: targeted edit loops only, such as `pnpm test <specific-file>`, targeted formatter checks, and small lint/type probes. If a local command expands beyond targeted proof, stop it and move the broad gate to Testbox.
- Testbox use: run from repo root, pre-warm early with `blacksmith testbox warmup ci-check-testbox.yml --ref main --idle-timeout 90`, reuse the returned `tbx_...` id for all `run`/`download` commands, and stop boxes you created before handoff. Timeout bins: `90` minutes default, `240` multi-hour, `720` all-day, `1440` overnight; anything above `1440` needs explicit approval and cleanup.
- Testbox full-suite profile: `blacksmith testbox run --id <ID> "env NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test"`. For installable package proof, prefer the GitHub `Package Acceptance` workflow over ad hoc Testbox commands.
- Local first. Use repo `pnpm` lanes before Blacksmith/Testbox. Remote only for parity-only failures, secrets/services, or explicit ask.
## GitHub / CI
@@ -72,7 +68,6 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
- GH comments with markdown backticks, `$`, or shell snippets: avoid inline double-quoted `--body`; use single quotes or `--body-file`.
- PR execution artifacts/screenshots: attach them to the PR, comment, or an external artifact store. Do not add `.github/pr-assets` or other PR-only assets to the repo.
- PR review answer must explicitly cover: what bug/behavior we are trying to fix; PR/issue URL(s) and affected endpoint/surface; whether this is the best possible fix, with high-certainty evidence from code, tests, CI, and shipped/current behavior.
- When working on an issue or PR, always end the user-facing final answer with the full GitHub URL.
- CI polling: exact SHA, needed fields only. Example: `gh api repos/<owner>/<repo>/actions/runs/<id> --jq '{status,conclusion,head_sha,updated_at,name,path}'`.
- Post-land wait: minimal. Exact landed SHA only. If superseded on `main`, same-branch `cancel-in-progress` cancellations are expected; stop once local touched-surface proof exists. Never wait for newer unrelated `main` unless asked.
- Wait matrix:
@@ -93,8 +88,7 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
- extension tests: extension test typecheck/tests
- public SDK/plugin contract: extension prod/test too
- unknown root/config: all lanes
- Before handoff/push for code/test/runtime/config changes: run `pnpm check:changed` in Testbox by default on maintainer machines. Tests-only: run `pnpm test:changed` in Testbox by default. Full prod sweep: run `pnpm check` in Testbox. Use local only for narrow targeted proof or when explicitly requested.
- If `pnpm test:changed` or `pnpm check:changed` selects broad/shared lanes, it belongs in Testbox; do not let it continue locally after it fans out.
- Before handoff/push for code/test/runtime/config changes: `pnpm check:changed`. Tests-only: `pnpm test:changed`. Full prod sweep: `pnpm check`.
- Docs/changelog-only and CI/workflow metadata-only changes are not changed-gate work by default. Use `git diff --check` plus the relevant formatter/docs/workflow sanity check; escalate to `pnpm check:changed` only when scripts, test config, generated docs/API, package metadata, or runtime/build behavior changed.
- Rebase sanity: after a green `pnpm check:changed`, a clean rebase onto current
`origin/main` does not require rerunning the full changed gate when the rebase
@@ -134,7 +128,6 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
- Prefer injection; if module mocking, mock narrow local `*.runtime.ts`, not broad barrels or `openclaw/plugin-sdk/*`.
- Share fixtures/builders; delete duplicate assertions; assert behavior that can regress here.
- Do not edit baseline/inventory/ignore/snapshot/expected-failure files to silence checks without explicit approval.
- Do not run multiple independent `pnpm test`/Vitest commands concurrently in the same worktree. They can race on `node_modules/.experimental-vitest-cache` and fail with `ENOTEMPTY`. Use one grouped `pnpm test ...` invocation, run targeted lanes sequentially, or set distinct `OPENCLAW_VITEST_FS_MODULE_CACHE_PATH` values when true parallel Vitest processes are needed.
- Test workers max 16. Memory pressure: `OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test`.
- Live: `OPENCLAW_LIVE_TEST=1 pnpm test:live`; verbose `OPENCLAW_LIVE_TEST_QUIET=0`.
- Guide: `docs/help/testing.md`.
@@ -142,9 +135,8 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
## Docs / Changelog
- Docs change with behavior/API. Use docs list/read_when hints; docs links per `docs/AGENTS.md`.
- Docs final answers: when doc files changed, end with the relevant full `https://docs.openclaw.ai/...` URL(s).
- Changelog user-facing only; fixing an issue or landing/merging a PR needs one unless pure test/internal.
- Changelog placement: active version `### Changes`/`### Fixes`; every added entry must include at least one `Thanks @author` attribution, using credited GitHub username(s). Never add `Thanks @codex`, `Thanks @openclaw`, or `Thanks @steipete`.
- Changelog user-facing only; pure test/internal usually no entry.
- Changelog placement: active version `### Changes`/`### Fixes`; every added entry must include at least one `Thanks @author` attribution, using credited GitHub username(s). Never add `Thanks @steipete` or `Thanks @codex`.
- Changelog bullets are always single-line. No wrapping/continuation across multiple lines. Long entries stay on one long line so dedupe, PR-ref, and credit-audit tooling work and so the visual style stays uniform.
## Git
@@ -176,7 +168,7 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
- Before simulator/emulator testing, check real iOS/Android devices.
- "restart iOS/Android apps" = rebuild/reinstall/relaunch, not kill/launch.
- SwiftUI: Observation (`@Observable`, `@Bindable`) over new `ObservableObject`.
- Mac gateway: dev watch = `pnpm gateway:watch` (tmux `openclaw-gateway-watch-main`, auto-attach). Noninteractive: `OPENCLAW_GATEWAY_WATCH_ATTACH=0 pnpm gateway:watch`; attach/stop: `tmux attach -t openclaw-gateway-watch-main` / `tmux kill-session -t openclaw-gateway-watch-main`. Managed installs: `openclaw gateway restart/status --deep`. No launchd/ad-hoc tmux. Logs: `./scripts/clawlog.sh`.
- Mac gateway: use app or `openclaw gateway restart/status --deep`; no ad-hoc tmux gateway. Logs: `./scripts/clawlog.sh`.
- Version bump touches: `package.json`, `apps/android/app/build.gradle.kts`, `apps/ios/version.json` + `pnpm ios:version:sync`, macOS `Info.plist`, `docs/install/updating.md`. Appcast only for Sparkle release.
- Mobile LAN pairing: plaintext `ws://` loopback-only. Private-network `ws://` needs `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1`; Tailscale/public use `wss://` or tunnel.
- A2UI hash `src/canvas-host/a2ui/.bundle.hash`: generated; ignore unless running `pnpm canvas:a2ui:bundle`; commit separately.
@@ -184,8 +176,6 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
## Ops / Footguns
- Remote install docs: `docs/install/{exe-dev,fly,hetzner}.md`. Parallels smoke: `$openclaw-parallels-smoke`; Discord roundtrip: `parallels-discord-roundtrip`.
- Memory wiki: keep prompt digest tiny. The prompt should only say the wiki exists, prefer `wiki_search` / `wiki_get`, start from `reports/person-agent-directory.md` for people routing, use search modes (`find-person`, `route-question`, `source-evidence`, `raw-claim`) when useful, and verify contact data before use.
- People wiki provenance: generated identity, social, contact, and "fun detail" notes need explicit source class/confidence (`maintainer-whois`, Discrawl sample/stat, GitHub profile, maintainer repo file). Do not promote inferred details to facts.
- Rebrand/migration/config warnings: run `openclaw doctor`.
- Never edit `node_modules`.
- Local-only `.agents` ignores: `.git/info/exclude`, not repo `.gitignore`.

File diff suppressed because it is too large Load Diff

View File

@@ -77,7 +77,7 @@ Welcome to the lobster tank! 🦞
- **Tengji (George) Zhang** - Chinese model APIs, cloud, pi
- GitHub: [@odysseus0](https://github.com/odysseus0) · X: [@odysseus0z](https://x.com/odysseus0z)
- **Sliverp** - Chinese Channel: QQ, WeChat, Wecom, Yuanbao, Dingtalk, Feishu
- **Sliverp** - Chinese Channel: QQ, WeChat, Wecom, Dingtalk, Feishu
- GitHub: [@sliverp](https://github.com/sliverp) · X: [@sliver01234](https://x.com/sliver01234)
- **Mason Huang** - Stability, Security, Speed

View File

@@ -63,8 +63,6 @@ COPY openclaw.mjs ./
COPY ui/package.json ./ui/package.json
COPY patches ./patches
COPY scripts/postinstall-bundled-plugins.mjs scripts/preinstall-package-manager-warning.mjs scripts/npm-runner.mjs scripts/windows-cmd-helpers.mjs ./scripts/
COPY scripts/lib/bundled-runtime-deps-install.mjs ./scripts/lib/bundled-runtime-deps-install.mjs
COPY scripts/lib/package-dist-imports.mjs ./scripts/lib/package-dist-imports.mjs
COPY --from=ext-deps /out/ ./${OPENCLAW_BUNDLED_PLUGIN_DIR}/
@@ -74,20 +72,10 @@ RUN --mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/sto
NODE_OPTIONS=--max-old-space-size=2048 pnpm install --frozen-lockfile
# pnpm v10+ may append peer-resolution hashes to virtual-store folder names; do not hardcode `.pnpm/...`
# paths. Matrix's native downloader can hit transient release CDN errors while
# still exiting successfully, so retry the package downloader before failing.
RUN set -eux; \
echo "==> Verifying critical native addons..."; \
for attempt in 1 2 3 4 5; do \
if find /app/node_modules -name "matrix-sdk-crypto*.node" 2>/dev/null | grep -q .; then \
exit 0; \
fi; \
echo "matrix-sdk-crypto native addon missing; retrying download (${attempt}/5)"; \
node /app/node_modules/@matrix-org/matrix-sdk-crypto-nodejs/download-lib.js || true; \
sleep $((attempt * 2)); \
done; \
# paths. Fail fast here if the Matrix native binding did not materialize after install.
RUN echo "==> Verifying critical native addons..." && \
find /app/node_modules -name "matrix-sdk-crypto*.node" 2>/dev/null | grep -q . || \
(echo "ERROR: matrix-sdk-crypto native addon missing after retries" >&2 && exit 1)
(echo "ERROR: matrix-sdk-crypto native addon missing (pnpm install may have silently failed on this arch)" >&2 && exit 1)
COPY . .
@@ -132,8 +120,7 @@ RUN printf 'packages:\n - .\n - ui\n' > /tmp/pnpm-workspace.runtime.yaml && \
cp /tmp/pnpm-workspace.runtime.yaml pnpm-workspace.yaml && \
CI=true NPM_CONFIG_FROZEN_LOCKFILE=false pnpm prune --prod && \
node scripts/postinstall-bundled-plugins.mjs && \
find dist -type f \( -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' -o -name '*.map' \) -delete && \
node scripts/check-package-dist-imports.mjs /app
find dist -type f \( -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' -o -name '*.map' \) -delete
# ── Runtime base image ──────────────────────────────────────────
FROM ${OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE} AS base-runtime
@@ -159,16 +146,11 @@ LABEL org.opencontainers.image.source="https://github.com/openclaw/openclaw" \
WORKDIR /app
# Install runtime system utilities missing from bookworm-slim.
# `ca-certificates` ships in `bookworm` (full) but not in `bookworm-slim`,
# so it must be installed explicitly here. Without it `/etc/ssl/certs/`
# stays empty and every HTTPS outbound dies at TLS handshake with
# `error setting certificate file`.
RUN --mount=type=cache,id=openclaw-bookworm-apt-cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=openclaw-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates procps hostname curl git lsof openssl python3 && \
update-ca-certificates
procps hostname curl git lsof openssl
RUN chown node:node /app
@@ -239,16 +221,9 @@ RUN --mount=type=cache,id=openclaw-bookworm-apt-cache,target=/var/cache/apt,shar
ca-certificates curl gnupg && \
install -m 0755 -d /etc/apt/keyrings && \
# Verify Docker apt signing key fingerprint before trusting it as a root key.
# Require exactly one primary key (`pub` in --with-colons; subkeys use `sub`) so we
# never pin the first fingerprint while apt trusts extra keys from the same file.
# Update OPENCLAW_DOCKER_GPG_FINGERPRINT when Docker rotates release keys.
curl -fsSL https://download.docker.com/linux/debian/gpg -o /tmp/docker.gpg.asc && \
expected_fingerprint="$(printf '%s' "$OPENCLAW_DOCKER_GPG_FINGERPRINT" | tr '[:lower:]' '[:upper:]' | tr -d '[:space:]')" && \
docker_gpg_pub_count="$(gpg --batch --show-keys --with-colons /tmp/docker.gpg.asc | awk -F: '$1 == "pub" { c++ } END { print c+0 }')" && \
if [ "$docker_gpg_pub_count" != "1" ]; then \
echo "ERROR: Docker apt key must contain exactly one public key (found $docker_gpg_pub_count); refusing a multi-key file." >&2; \
exit 1; \
fi && \
actual_fingerprint="$(gpg --batch --show-keys --with-colons /tmp/docker.gpg.asc | awk -F: '$1 == "fpr" { print toupper($10); exit }')" && \
if [ -z "$actual_fingerprint" ] || [ "$actual_fingerprint" != "$expected_fingerprint" ]; then \
echo "ERROR: Docker apt key fingerprint mismatch (expected $expected_fingerprint, got ${actual_fingerprint:-<empty>})" >&2; \
@@ -268,13 +243,6 @@ RUN --mount=type=cache,id=openclaw-bookworm-apt-cache,target=/var/cache/apt,shar
RUN ln -sf /app/openclaw.mjs /usr/local/bin/openclaw \
&& chmod 755 /app/openclaw.mjs
# Pre-create the default state and runtime-deps dirs so first-run Docker named
# volumes mounted here inherit node ownership instead of root-owned state.
RUN install -d -m 0700 -o node -g node /home/node/.openclaw && \
install -d -m 0700 -o node -g node /var/lib/openclaw/plugin-runtime-deps && \
stat -c '%U:%G %a' /home/node/.openclaw | grep -qx 'node:node 700' && \
stat -c '%U:%G %a' /var/lib/openclaw/plugin-runtime-deps | grep -qx 'node:node 700'
ENV NODE_ENV=production
# Security hardening: Run as non-root user

View File

@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1.7
FROM debian:bookworm-slim@sha256:f9c6a2fd2ddbc23e336b6257a5245e31f996953ef06cd13a59fa0a1df2d5c252
FROM debian:bookworm-slim@sha256:4724b8cc51e33e398f0e2e15e18d5ec2851ff0c2280647e1310bc1642182655d
ENV DEBIAN_FRONTEND=noninteractive

View File

@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1.7
FROM debian:bookworm-slim@sha256:f9c6a2fd2ddbc23e336b6257a5245e31f996953ef06cd13a59fa0a1df2d5c252
FROM debian:bookworm-slim@sha256:4724b8cc51e33e398f0e2e15e18d5ec2851ff0c2280647e1310bc1642182655d
ENV DEBIAN_FRONTEND=noninteractive

View File

@@ -1,14 +1,8 @@
# Security Policy
If you believe you've found a security issue in OpenClaw, report it privately first.
If you believe you've found a security issue in OpenClaw, please report it privately.
This policy does two things: it gives researchers a clear disclosure path, and it spells out the trust model maintainers use when triaging reports. OpenClaw is local-first agent infrastructure for trusted operators; it is not designed as a shared multi-tenant boundary between adversarial users on one gateway.
The fastest useful reports show a current, reproducible boundary bypass with demonstrated impact. Scanner output, prompt-injection-only chains, or reports that rely on hostile users sharing one trusted gateway are usually not security vulnerabilities under this model.
Security work is shared across a number of OpenClaw maintainers, including engineers and security researchers from organizations such as NVIDIA and Tencent. See the [maintainer list](CONTRIBUTING.md#maintainers).
## Report a Security Issue
## Reporting
Report vulnerabilities directly to the repository where the issue lives:
@@ -21,50 +15,22 @@ Report vulnerabilities directly to the repository where the issue lives:
For issues that don't fit a specific repo, or if you're unsure, email **[security@openclaw.ai](mailto:security@openclaw.ai)** and we'll route it.
For OpenClaw core issues, submit through a private [GitHub Security Advisory](https://github.com/openclaw/openclaw/security/advisories/new). Do not open a public issue or PR that discloses an unpatched vulnerability, exploit path, secret, or security-sensitive proof of concept.
Maintainers may close, hide, delete, or otherwise take down public issues and PRs that disclose vulnerabilities or active security issues. We will redirect those reports through the private disclosure process so the issue can be triaged and fixed without giving attackers a public playbook.
For full reporting instructions see our [Trust page](https://trust.openclaw.ai).
OpenClaw does not currently run a paid bug bounty program. Please still disclose responsibly so we can fix real issues quickly. The best way to help the project right now is to send high-signal reports and, when practical, focused PRs.
### Required in Reports
### What We Need
1. **Title**
2. **Severity Assessment**
3. **Impact**
4. **Affected Component**
5. **Technical Reproduction**
6. **Demonstrated Impact**
7. **Environment**
8. **Remediation Advice**
Make the report easy to reproduce and easy to route:
Reports without reproduction steps, demonstrated impact, and remediation advice will be deprioritized. Given the volume of AI-generated scanner findings, we must ensure we're receiving vetted reports from researchers who understand the issues.
- What you found and why you believe it is security-relevant.
- The affected component, version, and commit SHA when possible.
- Reproduction steps or a proof of concept against latest `main` or the latest released version.
- The actual impact, including which OpenClaw trust boundary is crossed.
- Any remediation advice or focused patch you can provide.
Reports without reproduction steps, demonstrated impact, and remediation advice are deprioritized. We receive a high volume of AI-generated scanner findings, so we prioritize vetted reports from researchers who can show how the issue crosses an OpenClaw security boundary.
### What Usually Is Not a Security Bug
These patterns are usually not vulnerabilities by themselves:
- Prompt injection without a policy, auth, approval, sandbox, or tool-boundary bypass.
- A trusted operator using an intentional local feature, such as local shell access or browser/script execution.
- A malicious plugin after a trusted operator installs or enables it.
- Multiple adversarial users sharing one Gateway host/config and expecting per-user isolation.
- Scanner-only, dependency-only, or stale-path reports without a working repro and demonstrated OpenClaw impact.
- Public internet exposure or risky deployment choices that the docs already recommend against.
If you are unsure, report privately. We would rather route a careful report than miss a real boundary issue.
### Duplicate Report Handling
- Search existing advisories before filing.
- Include likely duplicate GHSA IDs in your report when applicable.
- Maintainers may close lower-quality/later duplicates in favor of the earliest high-quality canonical report.
## Security Posture and Report Rules
The sections below are the normative posture maintainers use for report triage. The headings are editorial; the policy text defines the boundary.
### Detailed Report Acceptance Gate
### Report Acceptance Gate (Triage Fast Path)
For fastest triage, include all of the following:
@@ -81,7 +47,7 @@ For fastest triage, include all of the following:
Reports that miss these requirements may be closed as `invalid` or `no-action`.
### Detailed False-Positive Patterns
### Common False-Positive Patterns
These are frequently reported but are typically closed with no code change:
@@ -98,7 +64,6 @@ These are frequently reported but are typically closed with no code change:
- Reports that only show differences in heuristic detection/parity (for example obfuscation-pattern detection on one exec path but not another, such as `node.invoke -> system.run` parity gaps) without demonstrating bypass of auth, approvals, allowlist enforcement, sandboxing, or other documented trust boundaries.
- Reports that only show an ACP tool can indirectly execute, mutate, orchestrate sessions, or reach another tool/runtime without demonstrating bypass of ACP prompt/approval, allowlist enforcement, sandboxing, or another documented trust boundary. ACP silent approval is intentionally limited to narrow readonly classes; parity-only indirect-command findings are hardening, not vulnerabilities.
- Reports that only show untrusted media bytes reaching a maintained native decoder dependency (for example Sharp/libvips/libheif) without proving the shipped dependency version is vulnerable and demonstrating crash, memory corruption, data exposure, or a boundary bypass through OpenClaw. JavaScript header sniffing and image dimension fast-paths are preflight/UX checks, not the security boundary for native decoder correctness.
- Reports whose only impact is transient extra memory, CPU, or allocation work from decoding, base64 expansion, media transcoding, serialization, or other format conversion after the input was already accepted under OpenClaw's configured size/trust limits, including base64 decode-before-size-estimate findings. These are performance issues, not vulnerabilities, unless the report demonstrates unauthenticated amplification, bypass of configured limits, crash/process termination, persistent resource exhaustion, data exposure, or another documented boundary bypass.
- ReDoS/DoS claims that require trusted operator configuration input (for example catastrophic regex in `sessionFilter` or `logging.redactPatterns`) without a trust-boundary bypass.
- Archive/install extraction claims that require pre-existing local filesystem priming in trusted state (for example planting symlink/hardlink aliases under destination directories such as skills/tools paths) without showing an untrusted path that can create/control that primitive.
- Reports that depend on replacing or rewriting an already-approved executable path on a trusted host (same-path inode/content swap) without showing an untrusted path to perform that write.
@@ -110,13 +75,27 @@ These are frequently reported but are typically closed with no code change:
- Claims that Microsoft Teams `fileConsent/invoke` `uploadInfo.uploadUrl` is attacker-controlled without demonstrating one of: auth boundary bypass, a real authenticated Teams/Bot Framework event carrying attacker-chosen URL, or compromise of the Microsoft/Bot trust path.
- Scanner-only claims against stale/nonexistent paths, or claims without a working repro.
- Reports that restate an already-fixed issue against later released versions without showing the vulnerable path still exists in the shipped tag or published artifact for that later version.
- SSRF reports against the operator-managed HTTP/WebSocket proxy-routing feature whose only claim is that ordinary process-local HTTP clients (`fetch`, `node:http`, `node:https`, WebSocket clients, axios/got/node-fetch-style clients) can reach an internal, metadata, private, or otherwise sensitive destination when proxy routing is disabled, missing, or the operator-managed proxy policy allows it. For this feature, OpenClaw provides fail-closed proxy routing when enabled; the external proxy's destination policy is operator infrastructure, not an OpenClaw-controlled security boundary. See [Network proxy](https://docs.openclaw.ai/security/network-proxy).
### Maintainer GHSA Updates via CLI
### Duplicate Report Handling
- Search existing advisories before filing.
- Include likely duplicate GHSA IDs in your report when applicable.
- Maintainers may close lower-quality/later duplicates in favor of the earliest high-quality canonical report.
## Security & Trust
**Jamieson O'Reilly** ([@theonejvo](https://twitter.com/theonejvo)) is Security & Trust at OpenClaw. Jamieson is the founder of [Dvuln](https://dvuln.com) and brings extensive experience in offensive security, penetration testing, and security program development.
## Bug Bounties
OpenClaw is a labor of love. There is no bug bounty program and no budget for paid reports. Please still disclose responsibly so we can fix issues quickly.
The best way to help the project right now is by sending PRs.
## Maintainers: GHSA Updates via CLI
When patching a GHSA via `gh api`, include `X-GitHub-Api-Version: 2022-11-28` (or newer). Without it, some fields (notably CVSS) may not persist even if the request returns 200.
### Operator Trust Model
## Operator Trust Model (Important)
OpenClaw does **not** model one gateway as a multi-tenant, adversarial user boundary.
@@ -141,7 +120,7 @@ OpenClaw does **not** model one gateway as a multi-tenant, adversarial user boun
- Implicit exec calls (no explicit host in the tool call) follow the same behavior.
- This is expected in OpenClaw's one-user trusted-operator model. If you need isolation, enable sandbox mode (`non-main`/`all`) and keep strict tool policy.
### Trusted Plugins
## Trusted Plugin Concept (Core)
Plugins/extensions are part of OpenClaw's trusted computing base for a gateway.
@@ -149,7 +128,7 @@ Plugins/extensions are part of OpenClaw's trusted computing base for a gateway.
- Plugin behavior such as reading env/files or running host commands is expected inside this trust boundary.
- Security reports must show a boundary bypass (for example unauthenticated plugin load, allowlist/policy bypass, or sandbox/path-safety bypass), not only malicious behavior from a trusted-installed plugin.
### Out of Scope
## Out of Scope
- Public Internet Exposure
- Using OpenClaw in ways that the docs recommend not to
@@ -169,13 +148,11 @@ Plugins/extensions are part of OpenClaw's trusted computing base for a gateway.
- Reports whose only claim is that an ACP-exposed tool can indirectly execute commands, mutate host state, or reach another privileged tool/runtime without demonstrating a bypass of ACP prompt/approval, allowlist enforcement, sandboxing, or another documented trust boundary. These are hardening-only findings, not vulnerabilities.
- Reports whose only claim is that exec approvals do not semantically model every interpreter/runtime loader form, subcommand, flag combination, package script, or transitive module/config import. Exec approvals bind exact request context and best-effort direct local file operands; they are not a complete semantic model of everything a runtime may load.
- Reports whose only claim is parser reachability in an up-to-date maintained dependency without showing that the exact shipped dependency build is vulnerable. We keep native media dependencies current; dependency exposure alone is not a vulnerability.
- Reports whose only claim is resource overhead from decode/encode, base64 expansion, media transcoding, serialization, or format-conversion order after input has already passed the applicable configured acceptance limits, including base64 decode-before-size-estimate findings. These are performance-only and should be ignored for GHSA triage unless the report demonstrates unauthenticated amplification, limit bypass, crash/process termination, persistent exhaustion, data exposure, or another documented boundary bypass.
- Exposed secrets that are third-party/user-controlled credentials (not OpenClaw-owned and not granting access to OpenClaw-operated infrastructure/services) without demonstrated OpenClaw impact
- Reports whose only claim is host-side exec when sandbox runtime is disabled/unavailable (documented default behavior in the trusted-operator model), without a boundary bypass.
- Reports whose only claim is that a platform-provided upload destination URL is untrusted (for example Microsoft Teams `fileConsent/invoke` `uploadInfo.uploadUrl`) without proving attacker control in an authenticated production flow.
- SSRF reports limited to the operator-managed HTTP/WebSocket proxy-routing feature where the demonstrated mitigation is to enable/configure `proxy.enabled` with a filtering `proxy.proxyUrl`/`OPENCLAW_PROXY_URL`, or where impact depends on a permissive/misconfigured operator proxy. This only covers normal process-local HTTP(S)/WebSocket egress (`fetch`, Node HTTP(S), and similar JavaScript clients); non-HTTP egress and other features are assessed separately. See [Network proxy](https://docs.openclaw.ai/security/network-proxy).
### Deployment Assumptions
## Deployment Assumptions
OpenClaw security guidance assumes:
@@ -185,7 +162,7 @@ OpenClaw security guidance assumes:
- Authenticated Gateway callers are treated as trusted operators. Session identifiers (for example `sessionKey`) are routing controls, not per-user authorization boundaries.
- Multiple gateway instances can run on one machine, but the recommended model is clean per-user isolation (prefer one host/VPS per user).
### One-User Trust Model
## One-User Trust Model (Personal Assistant)
OpenClaw's security model is "personal assistant" (one trusted operator, potentially many agents), not "shared multi-tenant bus."
@@ -197,7 +174,7 @@ OpenClaw's security model is "personal assistant" (one trusted operator, potenti
- For company-shared setups, use a dedicated machine/VM/container and dedicated accounts; avoid mixing personal data on that runtime.
- If that host/browser profile is logged into personal accounts (for example Apple/Google/personal password manager), you have collapsed the boundary and increased personal-data exposure risk.
### Context Visibility and Allowlists
## Context Visibility and Allowlists
OpenClaw distinguishes:
@@ -215,7 +192,7 @@ Reports that only show supplemental-context visibility differences are typically
Hardening roadmap may add explicit visibility modes (for example `all`, `allowlist`, `allowlist_quote`) so operators can opt into stricter context filtering with predictable tradeoffs.
### Agent and Model Assumptions
## Agent and Model Assumptions
- The model/agent is **not** a trusted principal. Assume prompt/content injection can manipulate behavior.
- Security boundaries come from host/config trust, auth, tool policy, sandboxing, and exec approvals.
@@ -223,7 +200,7 @@ Hardening roadmap may add explicit visibility modes (for example `all`, `allowli
- Hook/webhook-driven payloads should be treated as untrusted content; keep unsafe bypass flags disabled unless doing tightly scoped debugging (`hooks.gmail.allowUnsafeExternalContent`, `hooks.mappings[].allowUnsafeExternalContent`).
- Weak model tiers are generally easier to prompt-inject. For tool-enabled or hook-driven agents, prefer strong modern model tiers and strict tool policy (for example `tools.profile: "messaging"` or stricter), plus sandboxing where possible.
### Gateway and Node Trust Concept
## Gateway and Node trust concept
OpenClaw separates routing from execution, but both remain inside the same operator trust boundary:
@@ -234,7 +211,7 @@ OpenClaw separates routing from execution, but both remain inside the same opera
- Differences in command-risk warning heuristics between exec surfaces (`gateway`, `node`, `sandbox`) do not, by themselves, constitute a security-boundary bypass.
- For untrusted-user isolation, split by trust boundary: separate gateways and separate OS users/hosts per boundary.
### Workspace Memory Trust Boundary
## Workspace Memory Trust Boundary
`MEMORY.md` and `memory/*.md` are plain workspace files and are treated as trusted local operator state.
@@ -243,7 +220,7 @@ OpenClaw separates routing from execution, but both remain inside the same opera
- Example report pattern considered out of scope: "attacker writes malicious content into `memory/*.md`, then `memory_search` returns it."
- If you need isolation between mutually untrusted users, split by OS user or host and run separate gateways.
### Plugin Trust Boundary
## Plugin Trust Boundary
Plugins/extensions are loaded **in-process** with the Gateway and are treated as trusted code.
@@ -251,7 +228,7 @@ Plugins/extensions are loaded **in-process** with the Gateway and are treated as
- Runtime helpers (for example `runtime.system.runCommandWithTimeout`) are convenience APIs, not a sandbox boundary.
- Only install plugins you trust, and prefer `plugins.allow` to pin explicit trusted plugin ids.
### Temp Folder Boundary
## Temp Folder Boundary (Media/Sandbox)
OpenClaw uses a dedicated temp root for local media handoff and sandbox-adjacent temp artifacts:
@@ -268,19 +245,19 @@ Security boundary notes:
- SDK temp helpers: `src/plugin-sdk/temp-path.ts`
- messaging/channel tmp guardrail: `scripts/check-no-random-messaging-tmp.mjs`
### Operational Guidance
## Operational Guidance
For threat model + hardening guidance (including `openclaw security audit --deep` and `--fix`), see:
- `https://docs.openclaw.ai/gateway/security`
#### Tool Filesystem Hardening
### Tool filesystem hardening
- `tools.exec.applyPatch.workspaceOnly: true` (recommended): keeps `apply_patch` writes/deletes within the configured workspace directory.
- `tools.fs.workspaceOnly: true` (optional): restricts `read`/`write`/`edit`/`apply_patch` paths and native prompt image auto-load paths to the workspace directory.
- Avoid setting `tools.exec.applyPatch.workspaceOnly: false` unless you fully trust who can trigger tool execution.
#### Sub-Agent Delegation Hardening
### Sub-agent delegation hardening
- Keep `sessions_spawn` denied unless you explicitly need delegated runs.
- Keep `agents.list[].subagents.allowAgents` narrow, and only include agents with sandbox settings you trust.
@@ -288,7 +265,7 @@ For threat model + hardening guidance (including `openclaw security audit --deep
- `sandbox: "require"` rejects the spawn unless the target child runtime is sandboxed.
- This prevents a less-restricted session from delegating work into an unsandboxed child by mistake.
#### Web Interface Safety
### Web Interface Safety
OpenClaw's web interface (Gateway Control UI + HTTP endpoints) is intended for **local use only**.
@@ -340,39 +317,12 @@ docker run --read-only --cap-drop=ALL \
## Security Scanning
OpenClaw uses several security and release-validation layers. No single scanner is treated as the boundary.
This project uses `detect-secrets` for automated secret detection in CI/CD.
See `.detect-secrets.cfg` for configuration and `.secrets.baseline` for the baseline.
### Secret Detection
OpenClaw uses `detect-secrets` with a checked-in baseline and local exclusion notes (`.secrets.baseline`, `.detect-secrets.cfg`). Secret-resolution behavior is also covered by the dedicated secrets test surface.
Run the baseline scan locally:
Run locally:
```bash
pip install detect-secrets==1.5.0
detect-secrets scan --baseline .secrets.baseline
```
### Static Analysis
CI runs CodeQL across core TypeScript, GitHub Actions, Android, macOS, and high-risk runtime boundaries using `.github/workflows/codeql*.yml` and `.github/codeql/*.yml`.
OpenGrep provides a high-precision Semgrep-compatible layer. PRs run a changed-path scan; maintainers can run a full repository scan when needed. The rulepack lives under `security/opengrep/`, with `.semgrepignore` as the shared exclusion file.
Run the local OpenGrep wrapper after installing `opengrep`:
```bash
scripts/run-opengrep.sh --changed --sarif --error
pnpm check:opengrep-rule-metadata
```
### E2E and Live Validation
Security-relevant behavior is also covered by runtime validation, not only static scanning:
- `pnpm test:e2e` for repo E2E coverage.
- `pnpm test:live` for live provider/runtime coverage.
- `pnpm test:docker:all` for Docker-packaged runtime scenarios.
- Package acceptance and scheduled live/E2E workflows for release-path validation.
These lanes exercise packaged installs, gateway/runtime behavior, live model/provider paths, Docker scenarios, and platform smoke tests. They complement scanners by proving the security-sensitive flows still behave correctly in real runtime environments.

View File

@@ -1,13 +1,31 @@
{
"originHash" : "646c710cf04fdf9e6c6ca935f3184924db3397a816848a7f8a8a3c10a4d8e9c8",
"originHash" : "24a723309d7a0039d3df3051106f77ac1ed7068a02508e3a6804e41d757e6c72",
"pins" : [
{
"identity" : "commander",
"kind" : "remoteSourceControl",
"location" : "https://github.com/steipete/Commander.git",
"state" : {
"revision" : "ae2ce746b386ff94b26648cfe5625cfa8d02639b",
"version" : "0.2.2"
"revision" : "9e349575c8e3c6745e81fe19e5bb5efa01b078ce",
"version" : "0.2.1"
}
},
{
"identity" : "elevenlabskit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/steipete/ElevenLabsKit",
"state" : {
"revision" : "7e3c948d8340abe3977014f3de020edf221e9269",
"version" : "0.1.0"
}
},
{
"identity" : "swift-concurrency-extras",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
"state" : {
"revision" : "5a3825302b1a0d744183200915a47b508c828e6f",
"version" : "1.3.2"
}
},
{
@@ -15,8 +33,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-syntax.git",
"state" : {
"revision" : "9de99a78f099e59caf2b2beec65a4c45d54b2081",
"version" : "603.0.1"
"revision" : "0687f71944021d616d34d922343dcef086855920",
"version" : "600.0.1"
}
},
{
@@ -24,8 +42,26 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-testing",
"state" : {
"revision" : "937120cbc281cf29727fdfb8734482158508b4fc",
"version" : "6.3.1"
"revision" : "399f76dcd91e4c688ca2301fa24a8cc6d9927211",
"version" : "0.99.0"
}
},
{
"identity" : "swiftui-math",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/swiftui-math",
"state" : {
"revision" : "0b5c2cfaaec8d6193db206f675048eeb5ce95f71",
"version" : "0.1.0"
}
},
{
"identity" : "textual",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/textual",
"state" : {
"revision" : "5b06b811c0f5313b6b84bbef98c635a630638c38",
"version" : "0.3.1"
}
}
],

View File

@@ -13,8 +13,8 @@ let package = Package(
.executable(name: "swabble", targets: ["SwabbleCLI"]),
],
dependencies: [
.package(url: "https://github.com/steipete/Commander.git", exact: "0.2.2"),
.package(url: "https://github.com/apple/swift-testing", from: "6.3.1"),
.package(url: "https://github.com/steipete/Commander.git", exact: "0.2.1"),
.package(url: "https://github.com/apple/swift-testing", from: "0.99.0"),
],
targets: [
.target(
@@ -43,6 +43,7 @@ let package = Package(
],
swiftSettings: [
.enableUpcomingFeature("StrictConcurrency"),
.enableExperimentalFeature("SwiftTesting"),
]),
.testTarget(
name: "swabbleTests",

View File

@@ -45,15 +45,6 @@ extension AttributedString {
}
return ranges.compactMap { range in
guard #available(macOS 26.0, iOS 26.0, *) else {
return AttributedString(self[range].characters)
}
return self.sentenceWithAudioTimeRange(range)
}
}
@available(macOS 26.0, iOS 26.0, *)
private func sentenceWithAudioTimeRange(_ range: Range<AttributedString.Index>) -> AttributedString? {
let audioTimeRanges = self[range].runs.filter {
!String(self[$0.range].characters)
.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
@@ -66,5 +57,6 @@ extension AttributedString {
start: start,
end: end)
return AttributedString(self[range].characters, attributes: attributes)
}
}
}

View File

@@ -17,35 +17,29 @@ public enum OutputFormat: String {
case .txt:
return String(transcript.characters)
case .srt:
guard #available(macOS 26.0, iOS 26.0, *) else { return "" }
return self.srtText(for: transcript, maxLength: maxLength)
func format(_ timeInterval: TimeInterval) -> String {
let ms = Int(timeInterval.truncatingRemainder(dividingBy: 1) * 1000)
let s = Int(timeInterval) % 60
let m = (Int(timeInterval) / 60) % 60
let h = Int(timeInterval) / 60 / 60
return String(format: "%0.2d:%0.2d:%0.2d,%0.3d", h, m, s, ms)
}
return transcript.sentences(maxLength: maxLength).compactMap { (sentence: AttributedString) -> (
CMTimeRange,
String)? in
guard let timeRange = sentence.audioTimeRange else { return nil }
return (timeRange, String(sentence.characters))
}.enumerated().map { index, run in
let (timeRange, text) = run
return """
\(index + 1)
\(format(timeRange.start.seconds)) --> \(format(timeRange.end.seconds))
\(text.trimmingCharacters(in: .whitespacesAndNewlines))
"""
}.joined().trimmingCharacters(in: .whitespacesAndNewlines)
}
}
@available(macOS 26.0, iOS 26.0, *)
private func srtText(for transcript: AttributedString, maxLength: Int) -> String {
func format(_ timeInterval: TimeInterval) -> String {
let ms = Int(timeInterval.truncatingRemainder(dividingBy: 1) * 1000)
let s = Int(timeInterval) % 60
let m = (Int(timeInterval) / 60) % 60
let h = Int(timeInterval) / 60 / 60
return String(format: "%0.2d:%0.2d:%0.2d,%0.3d", h, m, s, ms)
}
return transcript.sentences(maxLength: maxLength).compactMap { (sentence: AttributedString) -> (
CMTimeRange,
String)? in
guard let timeRange = sentence.audioTimeRange else { return nil }
return (timeRange, String(sentence.characters))
}.enumerated().map { index, run in
let (timeRange, text) = run
return """
\(index + 1)
\(format(timeRange.start.seconds)) --> \(format(timeRange.end.seconds))
\(text.trimmingCharacters(in: .whitespacesAndNewlines))
"""
}.joined().trimmingCharacters(in: .whitespacesAndNewlines)
}
}

View File

@@ -13,9 +13,7 @@ public struct WakeWordSegment: Sendable, Equatable {
self.range = range
}
public var end: TimeInterval {
self.start + self.duration
}
public var end: TimeInterval { start + duration }
}
public struct WakeWordGateConfig: Sendable, Equatable {
@@ -26,8 +24,7 @@ public struct WakeWordGateConfig: Sendable, Equatable {
public init(
triggers: [String],
minPostTriggerGap: TimeInterval = 0.45,
minCommandLength: Int = 1)
{
minCommandLength: Int = 1) {
self.triggers = triggers
self.minPostTriggerGap = minPostTriggerGap
self.minCommandLength = minCommandLength
@@ -81,10 +78,10 @@ public enum WakeWordGate {
segments: [WakeWordSegment],
config: WakeWordGateConfig)
-> WakeWordGateMatch? {
let triggerTokens = self.normalizeTriggers(config.triggers)
let triggerTokens = normalizeTriggers(config.triggers)
guard !triggerTokens.isEmpty else { return nil }
let tokens = self.normalizeSegments(segments)
let tokens = normalizeSegments(segments)
guard !tokens.isEmpty else { return nil }
var best: MatchCandidate?
@@ -118,7 +115,7 @@ public enum WakeWordGate {
}
guard let best else { return nil }
let command = self.commandText(transcript: transcript, segments: segments, triggerEndTime: best.triggerEnd)
let command = commandText(transcript: transcript, segments: segments, triggerEndTime: best.triggerEnd)
.trimmingCharacters(in: Self.whitespaceAndPunctuation)
guard command.count >= config.minCommandLength else { return nil }
return WakeWordGateMatch(
@@ -148,7 +145,7 @@ public enum WakeWordGate {
guard !text.isEmpty else { return false }
let normalized = text.lowercased()
for trigger in triggers {
let token = trigger.trimmingCharacters(in: self.whitespaceAndPunctuation).lowercased()
let token = trigger.trimmingCharacters(in: whitespaceAndPunctuation).lowercased()
if token.isEmpty { continue }
if normalized.contains(token) { return true }
}
@@ -158,11 +155,11 @@ public enum WakeWordGate {
public static func stripWake(text: String, triggers: [String]) -> String {
var out = text
for trigger in triggers {
let token = trigger.trimmingCharacters(in: self.whitespaceAndPunctuation)
let token = trigger.trimmingCharacters(in: whitespaceAndPunctuation)
guard !token.isEmpty else { continue }
out = out.replacingOccurrences(of: token, with: "", options: [.caseInsensitive])
}
return out.trimmingCharacters(in: self.whitespaceAndPunctuation)
return out.trimmingCharacters(in: whitespaceAndPunctuation)
}
private static func normalizeTriggers(_ triggers: [String]) -> [TriggerTokens] {
@@ -170,7 +167,7 @@ public enum WakeWordGate {
for trigger in triggers {
let tokens = trigger
.split(whereSeparator: { $0.isWhitespace })
.map { self.normalizeToken(String($0)) }
.map { normalizeToken(String($0)) }
.filter { !$0.isEmpty }
if tokens.isEmpty { continue }
output.append(TriggerTokens(source: tokens.joined(separator: " "), tokens: tokens))
@@ -180,7 +177,7 @@ public enum WakeWordGate {
private static func normalizeSegments(_ segments: [WakeWordSegment]) -> [Token] {
segments.compactMap { segment in
let normalized = self.normalizeToken(segment.text)
let normalized = normalizeToken(segment.text)
guard !normalized.isEmpty else { return nil }
return Token(
normalized: normalized,
@@ -193,7 +190,7 @@ public enum WakeWordGate {
private static func normalizeToken(_ token: String) -> String {
token
.trimmingCharacters(in: self.whitespaceAndPunctuation)
.trimmingCharacters(in: whitespaceAndPunctuation)
.lowercased()
}

View File

@@ -5,7 +5,6 @@ import Speech
import Swabble
@MainActor
@available(macOS 26.0, *)
struct TranscribeCommand: ParsableCommand {
@Argument(help: "Path to audio/video file") var inputFile: String = ""
@Option(name: .long("locale"), help: "Locale identifier", parsing: .singleValue) var locale: String = Locale.current

Some files were not shown because too many files have changed in this diff Show More