From f43cdb0a6e9205122bd27bb982ab40443999a81e Mon Sep 17 00:00:00 2001 From: kumarabhirup Date: Mon, 16 Feb 2026 23:13:23 -0800 Subject: [PATCH] chore: strip upstream-only GitHub Actions and slim CI Remove workflows irrelevant to this fork (auto-response, stale, labeler, docker-release, install-smoke, formal-conformance, sandbox-common-smoke). Slim ci.yml to just check + test jobs, dropping iOS/macOS/Android/Windows/secrets/docs/release lanes. Clean dependabot.yml of Swift and Gradle entries. Co-authored-by: Cursor --- .github/dependabot.yml | 64 -- .github/workflows/auto-response.yml | 224 ------- .github/workflows/ci.yml | 664 +-------------------- .github/workflows/docker-release.yml | 149 ----- .github/workflows/formal-conformance.yml | 139 ----- .github/workflows/install-smoke.yml | 59 -- .github/workflows/labeler.yml | 519 ---------------- .github/workflows/sandbox-common-smoke.yml | 56 -- .github/workflows/stale.yml | 51 -- 9 files changed, 11 insertions(+), 1914 deletions(-) delete mode 100644 .github/workflows/auto-response.yml delete mode 100644 .github/workflows/docker-release.yml delete mode 100644 .github/workflows/formal-conformance.yml delete mode 100644 .github/workflows/install-smoke.yml delete mode 100644 .github/workflows/labeler.yml delete mode 100644 .github/workflows/sandbox-common-smoke.yml delete mode 100644 .github/workflows/stale.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 829604b4ce3..e5a410a3107 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -47,67 +47,3 @@ updates: - minor - patch open-pull-requests-limit: 5 - - # Swift Package Manager - macOS app - - package-ecosystem: swift - directory: /apps/macos - schedule: - interval: weekly - cooldown: - default-days: 7 - groups: - swift-deps: - patterns: - - "*" - update-types: - - minor - - patch - open-pull-requests-limit: 5 - - # Swift Package Manager - shared MoltbotKit - - package-ecosystem: swift - directory: /apps/shared/MoltbotKit - schedule: - interval: weekly - cooldown: - default-days: 7 - groups: - swift-deps: - patterns: - - "*" - update-types: - - minor - - patch - open-pull-requests-limit: 5 - - # Swift Package Manager - Swabble - - package-ecosystem: swift - directory: /Swabble - schedule: - interval: weekly - cooldown: - default-days: 7 - groups: - swift-deps: - patterns: - - "*" - update-types: - - minor - - patch - open-pull-requests-limit: 5 - - # Gradle - Android app - - package-ecosystem: gradle - directory: /apps/android - schedule: - interval: weekly - cooldown: - default-days: 7 - groups: - android-deps: - patterns: - - "*" - update-types: - - minor - - patch - open-pull-requests-limit: 5 diff --git a/.github/workflows/auto-response.yml b/.github/workflows/auto-response.yml deleted file mode 100644 index e3987c500c3..00000000000 --- a/.github/workflows/auto-response.yml +++ /dev/null @@ -1,224 +0,0 @@ -name: Auto response - -on: - issues: - types: [opened, edited, labeled] - pull_request_target: - types: [labeled] - -permissions: {} - -jobs: - auto-response: - permissions: - issues: write - pull-requests: write - runs-on: ubuntu-latest - steps: - - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 - id: app-token - with: - app-id: "2729701" - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - name: Handle labeled items - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 - with: - github-token: ${{ steps.app-token.outputs.token }} - script: | - // Labels prefixed with "r:" are auto-response triggers. - const rules = [ - { - label: "r: skill", - close: true, - message: - "Thanks for the contribution! New skills should be published to [Clawhub](https://clawhub.ai) for everyone to use. We’re keeping the core lean on skills, so I’m closing this out.", - }, - { - label: "r: support", - close: true, - message: - "Please use [our support server](https://discord.gg/clawd) and ask in #help or #users-helping-users to resolve this, or follow the stuck FAQ at https://docs.openclaw.ai/help/faq#im-stuck-whats-the-fastest-way-to-get-unstuck.", - }, - { - label: "r: testflight", - close: true, - message: "Not available, build from source.", - }, - { - label: "r: third-party-extension", - close: true, - message: - "This would be better made as a third-party extension with our SDK that you maintain yourself. Docs: https://docs.openclaw.ai/plugin.", - }, - { - label: "r: moltbook", - close: true, - lock: true, - lockReason: "off-topic", - message: - "OpenClaw is not affiliated with Moltbook, and issues related to Moltbook should not be submitted here.", - }, - ]; - - const triggerLabel = "trigger-response"; - const target = context.payload.issue ?? context.payload.pull_request; - if (!target) { - return; - } - - const labelSet = new Set( - (target.labels ?? []) - .map((label) => (typeof label === "string" ? label : label?.name)) - .filter((name) => typeof name === "string"), - ); - - const hasTriggerLabel = labelSet.has(triggerLabel); - if (hasTriggerLabel) { - labelSet.delete(triggerLabel); - try { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: target.number, - name: triggerLabel, - }); - } catch (error) { - if (error?.status !== 404) { - throw error; - } - } - } - - const isLabelEvent = context.payload.action === "labeled"; - if (!hasTriggerLabel && !isLabelEvent) { - return; - } - - const issue = context.payload.issue; - if (issue) { - const title = issue.title ?? ""; - const body = issue.body ?? ""; - const haystack = `${title}\n${body}`.toLowerCase(); - const hasMoltbookLabel = labelSet.has("r: moltbook"); - const hasTestflightLabel = labelSet.has("r: testflight"); - const hasSecurityLabel = labelSet.has("security"); - if (title.toLowerCase().includes("security") && !hasSecurityLabel) { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - labels: ["security"], - }); - labelSet.add("security"); - } - if (title.toLowerCase().includes("testflight") && !hasTestflightLabel) { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - labels: ["r: testflight"], - }); - labelSet.add("r: testflight"); - } - if (haystack.includes("moltbook") && !hasMoltbookLabel) { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - labels: ["r: moltbook"], - }); - labelSet.add("r: moltbook"); - } - } - - const invalidLabel = "invalid"; - const dirtyLabel = "dirty"; - const noisyPrMessage = - "Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch."; - - const pullRequest = context.payload.pull_request; - if (pullRequest) { - if (labelSet.has(dirtyLabel)) { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pullRequest.number, - body: noisyPrMessage, - }); - await github.rest.issues.update({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pullRequest.number, - state: "closed", - }); - return; - } - const labelCount = labelSet.size; - if (labelCount > 20) { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pullRequest.number, - body: noisyPrMessage, - }); - await github.rest.issues.update({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pullRequest.number, - state: "closed", - }); - return; - } - if (labelSet.has(invalidLabel)) { - await github.rest.issues.update({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pullRequest.number, - state: "closed", - }); - return; - } - } - - if (issue && labelSet.has(invalidLabel)) { - await github.rest.issues.update({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - state: "closed", - state_reason: "not_planned", - }); - return; - } - - const rule = rules.find((item) => labelSet.has(item.label)); - if (!rule) { - return; - } - - const issueNumber = target.number; - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - body: rule.message, - }); - - if (rule.close) { - await github.rest.issues.update({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - state: "closed", - }); - } - - if (rule.lock) { - await github.rest.issues.lock({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - lock_reason: rule.lockReason ?? "resolved", - }); - } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f57a9a7447..10477edcd05 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,232 +10,9 @@ concurrency: cancel-in-progress: true jobs: - # Detect docs-only changes to skip heavy jobs (test, build, Windows, macOS, Android). - # Lint and format always run. Fail-safe: if detection fails, run everything. - docs-scope: - runs-on: ubuntu-latest - outputs: - docs_only: ${{ steps.check.outputs.docs_only }} - docs_changed: ${{ steps.check.outputs.docs_changed }} - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - submodules: false - - - name: Detect docs-only changes - id: check - uses: ./.github/actions/detect-docs-changes - - # Detect which heavy areas are touched so PRs can skip unrelated expensive jobs. - # Push to main keeps broad coverage. - changed-scope: - needs: [docs-scope] - if: needs.docs-scope.outputs.docs_only != 'true' - runs-on: ubuntu-latest - outputs: - run_node: ${{ steps.scope.outputs.run_node }} - run_macos: ${{ steps.scope.outputs.run_macos }} - run_android: ${{ steps.scope.outputs.run_android }} - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - submodules: false - - - name: Detect changed scopes - id: scope - shell: bash - run: | - set -euo pipefail - - if [ "${{ github.event_name }}" = "push" ]; then - BASE="${{ github.event.before }}" - else - BASE="${{ github.event.pull_request.base.sha }}" - fi - - CHANGED="$(git diff --name-only "$BASE" HEAD 2>/dev/null || echo "UNKNOWN")" - if [ "$CHANGED" = "UNKNOWN" ] || [ -z "$CHANGED" ]; then - # Fail-safe: run broad checks if detection fails. - echo "run_node=true" >> "$GITHUB_OUTPUT" - echo "run_macos=true" >> "$GITHUB_OUTPUT" - echo "run_android=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - - run_node=false - run_macos=false - run_android=false - has_non_docs=false - has_non_native_non_docs=false - - while IFS= read -r path; do - [ -z "$path" ] && continue - case "$path" in - docs/*|*.md|*.mdx) - continue - ;; - *) - has_non_docs=true - ;; - esac - - case "$path" in - # Generated protocol models are already covered by protocol:check and - # should not force the full native macOS lane. - apps/macos/Sources/OpenClawProtocol/*|apps/shared/OpenClawKit/Sources/OpenClawProtocol/*) - ;; - apps/macos/*|apps/ios/*|apps/shared/*|Swabble/*) - run_macos=true - ;; - esac - - case "$path" in - apps/android/*|apps/shared/*) - run_android=true - ;; - esac - - case "$path" in - src/*|test/*|extensions/*|packages/*|scripts/*|ui/*|.github/*|openclaw.mjs|package.json|pnpm-lock.yaml|pnpm-workspace.yaml|tsconfig*.json|vitest*.ts|tsdown.config.ts|.oxlintrc.json|.oxfmtrc.jsonc) - run_node=true - ;; - esac - - case "$path" in - apps/android/*|apps/ios/*|apps/macos/*|apps/shared/*|Swabble/*|appcast.xml) - ;; - *) - has_non_native_non_docs=true - ;; - esac - done <<< "$CHANGED" - - # If there are non-doc files outside native app trees, keep Node checks enabled. - if [ "$run_node" = false ] && [ "$has_non_docs" = true ] && [ "$has_non_native_non_docs" = true ]; then - run_node=true - fi - - echo "run_node=${run_node}" >> "$GITHUB_OUTPUT" - echo "run_macos=${run_macos}" >> "$GITHUB_OUTPUT" - echo "run_android=${run_android}" >> "$GITHUB_OUTPUT" - - # Build dist once for Node-relevant changes and share it with downstream jobs. - build-artifacts: - needs: [docs-scope, changed-scope, check] - if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true') - runs-on: blacksmith-4vcpu-ubuntu-2404 - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: false - - - name: Setup Node environment - uses: ./.github/actions/setup-node-env - with: - install-bun: "false" - - - name: Build dist - run: pnpm build - - - name: Upload dist artifact - uses: actions/upload-artifact@v4 - with: - name: dist-build - path: dist/ - retention-days: 1 - - # Validate npm pack contents after build (only on push to main, not PRs). - release-check: - needs: [docs-scope, build-artifacts] - if: github.event_name == 'push' && needs.docs-scope.outputs.docs_only != 'true' - runs-on: blacksmith-4vcpu-ubuntu-2404 - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: false - - - name: Setup Node environment - uses: ./.github/actions/setup-node-env - with: - install-bun: "false" - - - name: Download dist artifact - uses: actions/download-artifact@v4 - with: - name: dist-build - path: dist/ - - - name: Check release contents - run: pnpm release:check - - checks: - needs: [docs-scope, changed-scope, check] - if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true') - runs-on: blacksmith-4vcpu-ubuntu-2404 - strategy: - fail-fast: false - matrix: - include: - - runtime: node - task: test - command: pnpm canvas:a2ui:bundle && pnpm test - - runtime: node - task: protocol - command: pnpm protocol:check - - runtime: bun - task: test - command: pnpm canvas:a2ui:bundle && bunx vitest run --config vitest.unit.config.ts - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: false - - - name: Setup Node environment - uses: ./.github/actions/setup-node-env - - - name: Configure vitest JSON reports - if: matrix.task == 'test' && matrix.runtime == 'node' - run: echo "OPENCLAW_VITEST_REPORT_DIR=$RUNNER_TEMP/vitest-reports" >> "$GITHUB_ENV" - - - name: Configure Node test resources - if: matrix.task == 'test' && matrix.runtime == 'node' - run: | - # `pnpm test` runs `scripts/test-parallel.mjs`, which spawns multiple Node processes. - # Default heap limits have been too low on Linux CI (V8 OOM near 4GB). - echo "OPENCLAW_TEST_WORKERS=2" >> "$GITHUB_ENV" - echo "OPENCLAW_TEST_MAX_OLD_SPACE_SIZE_MB=6144" >> "$GITHUB_ENV" - - - name: Run ${{ matrix.task }} (${{ matrix.runtime }}) - run: ${{ matrix.command }} - - - name: Summarize slowest tests - if: matrix.task == 'test' && matrix.runtime == 'node' - run: | - node scripts/vitest-slowest.mjs --dir "$OPENCLAW_VITEST_REPORT_DIR" --top 50 --out "$RUNNER_TEMP/vitest-slowest.md" > /dev/null - echo "Slowest test summary written to $RUNNER_TEMP/vitest-slowest.md" - - - name: Upload vitest reports - if: matrix.task == 'test' && matrix.runtime == 'node' - uses: actions/upload-artifact@v4 - with: - name: vitest-reports-${{ runner.os }}-${{ matrix.runtime }} - path: | - ${{ env.OPENCLAW_VITEST_REPORT_DIR }} - ${{ runner.temp }}/vitest-slowest.md - - # Types, lint, and format check. check: name: "check" - needs: [docs-scope] - if: needs.docs-scope.outputs.docs_only != 'true' - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 @@ -248,179 +25,17 @@ jobs: - name: Check types and lint and oxfmt run: pnpm check - # Validate docs (format, lint, broken links) only when docs files changed. - check-docs: - needs: [docs-scope] - if: needs.docs-scope.outputs.docs_changed == 'true' - runs-on: blacksmith-4vcpu-ubuntu-2404 - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: false - - - name: Setup Node environment - uses: ./.github/actions/setup-node-env - - - name: Check docs - run: pnpm check:docs - - secrets: - runs-on: blacksmith-4vcpu-ubuntu-2404 - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: false - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Install detect-secrets - run: | - python -m pip install --upgrade pip - python -m pip install detect-secrets==1.5.0 - - - name: Detect secrets - run: | - if ! detect-secrets scan --baseline .secrets.baseline; then - echo "::error::Secret scanning failed. See docs/gateway/security.md#secret-scanning-detect-secrets" - exit 1 - fi - - checks-windows: - needs: [docs-scope, changed-scope, build-artifacts, check] - if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true') - runs-on: blacksmith-4vcpu-windows-2025 - env: - NODE_OPTIONS: --max-old-space-size=4096 - # Keep total concurrency predictable on the 4 vCPU runner: - # `scripts/test-parallel.mjs` runs some vitest suites in parallel processes. - OPENCLAW_TEST_WORKERS: 2 - defaults: - run: - shell: bash + test: + needs: [check] + runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - runtime: node - task: lint - command: pnpm lint - - runtime: node - task: test command: pnpm canvas:a2ui:bundle && pnpm test - - runtime: node - task: protocol - command: pnpm protocol:check - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: false - - - name: Try to exclude workspace from Windows Defender (best-effort) - shell: pwsh - run: | - $cmd = Get-Command Add-MpPreference -ErrorAction SilentlyContinue - if (-not $cmd) { - Write-Host "Add-MpPreference not available, skipping Defender exclusions." - exit 0 - } - - try { - # Defender sometimes intercepts process spawning (vitest workers). If this fails - # (eg hardened images), keep going and rely on worker limiting above. - Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE" -ErrorAction Stop - Add-MpPreference -ExclusionProcess "node.exe" -ErrorAction Stop - Write-Host "Defender exclusions applied." - } catch { - Write-Warning "Failed to apply Defender exclusions, continuing. $($_.Exception.Message)" - } - - - name: Download dist artifact (lint lane) - if: matrix.task == 'lint' - uses: actions/download-artifact@v4 - with: - name: dist-build - path: dist/ - - - name: Verify dist artifact (lint lane) - if: matrix.task == 'lint' - run: | - set -euo pipefail - test -s dist/index.js - test -s dist/plugin-sdk/index.js - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22.x - check-latest: true - - - name: Setup pnpm + cache store - uses: ./.github/actions/setup-pnpm-store-cache - with: - pnpm-version: "10.23.0" - cache-key-suffix: "node22" - - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - - name: Runtime versions - run: | - node -v - npm -v - bun -v - pnpm -v - - - name: Capture node path - run: echo "NODE_BIN=$(dirname \"$(node -p \"process.execPath\")\")" >> "$GITHUB_ENV" - - - name: Install dependencies - env: - CI: true - run: | - export PATH="$NODE_BIN:$PATH" - which node - node -v - pnpm -v - pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true || pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true - - - name: Configure vitest JSON reports - if: matrix.task == 'test' - run: echo "OPENCLAW_VITEST_REPORT_DIR=$RUNNER_TEMP/vitest-reports" >> "$GITHUB_ENV" - - - name: Run ${{ matrix.task }} (${{ matrix.runtime }}) - run: ${{ matrix.command }} - - - name: Summarize slowest tests - if: matrix.task == 'test' - run: | - node scripts/vitest-slowest.mjs --dir "$OPENCLAW_VITEST_REPORT_DIR" --top 50 --out "$RUNNER_TEMP/vitest-slowest.md" > /dev/null - echo "Slowest test summary written to $RUNNER_TEMP/vitest-slowest.md" - - - name: Upload vitest reports - if: matrix.task == 'test' - uses: actions/upload-artifact@v4 - with: - name: vitest-reports-${{ runner.os }}-${{ matrix.runtime }} - path: | - ${{ env.OPENCLAW_VITEST_REPORT_DIR }} - ${{ runner.temp }}/vitest-slowest.md - - # Consolidated macOS job: runs TS tests + Swift lint/build/test sequentially - # on a single runner. GitHub limits macOS concurrent jobs to 5 per org; - # running 4 separate jobs per PR (as before) starved the queue. One job - # per PR allows 5 PRs to run macOS checks simultaneously. - macos: - needs: [docs-scope, changed-scope, check] - if: github.event_name == 'pull_request' && needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_macos == 'true' - runs-on: macos-latest + - runtime: bun + command: pnpm canvas:a2ui:bundle && bunx vitest run --config vitest.unit.config.ts steps: - name: Checkout uses: actions/checkout@v4 @@ -429,269 +44,12 @@ jobs: - name: Setup Node environment uses: ./.github/actions/setup-node-env - with: - install-bun: "false" - # --- Run all checks sequentially (fast gates first) --- - - name: TS tests (macOS) - env: - NODE_OPTIONS: --max-old-space-size=4096 - run: pnpm test - - # --- Xcode/Swift setup --- - - name: Select Xcode 26.1 + - name: Configure Node test resources + if: matrix.runtime == 'node' run: | - sudo xcode-select -s /Applications/Xcode_26.1.app - xcodebuild -version + echo "OPENCLAW_TEST_WORKERS=2" >> "$GITHUB_ENV" + echo "OPENCLAW_TEST_MAX_OLD_SPACE_SIZE_MB=8192" >> "$GITHUB_ENV" - - name: Install XcodeGen / SwiftLint / SwiftFormat - run: brew install xcodegen swiftlint swiftformat - - - name: Show toolchain - run: | - sw_vers - xcodebuild -version - swift --version - - - name: Swift lint - run: | - swiftlint --config .swiftlint.yml - swiftformat --lint apps/macos/Sources --config .swiftformat - - - name: Cache SwiftPM - uses: actions/cache@v4 - with: - path: ~/Library/Caches/org.swift.swiftpm - key: ${{ runner.os }}-swiftpm-${{ hashFiles('apps/macos/Package.resolved') }} - restore-keys: | - ${{ runner.os }}-swiftpm- - - - name: Swift build (release) - run: | - set -euo pipefail - for attempt in 1 2 3; do - if swift build --package-path apps/macos --configuration release; then - exit 0 - fi - echo "swift build failed (attempt $attempt/3). Retrying…" - sleep $((attempt * 20)) - done - exit 1 - - - name: Swift test - run: | - set -euo pipefail - for attempt in 1 2 3; do - if swift test --package-path apps/macos --parallel --enable-code-coverage --show-codecov-path; then - exit 0 - fi - echo "swift test failed (attempt $attempt/3). Retrying…" - sleep $((attempt * 20)) - done - exit 1 - - ios: - if: false # ignore iOS in CI for now - runs-on: macos-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: false - - - name: Select Xcode 26.1 - run: | - sudo xcode-select -s /Applications/Xcode_26.1.app - xcodebuild -version - - - name: Install XcodeGen - run: brew install xcodegen - - - name: Install SwiftLint / SwiftFormat - run: brew install swiftlint swiftformat - - - name: Show toolchain - run: | - sw_vers - xcodebuild -version - swift --version - - - name: Generate iOS project - run: | - cd apps/ios - xcodegen generate - - - name: iOS tests - run: | - set -euo pipefail - RESULT_BUNDLE_PATH="$RUNNER_TEMP/Clawdis-iOS.xcresult" - DEST_ID="$( - python3 - <<'PY' - import json - import subprocess - import sys - import uuid - - def sh(args: list[str]) -> str: - return subprocess.check_output(args, text=True).strip() - - # Prefer an already-created iPhone simulator if it exists. - devices = json.loads(sh(["xcrun", "simctl", "list", "devices", "-j"])) - candidates: list[tuple[str, str]] = [] - for runtime, devs in (devices.get("devices") or {}).items(): - for dev in devs or []: - if not dev.get("isAvailable"): - continue - name = str(dev.get("name") or "") - udid = str(dev.get("udid") or "") - if not udid or not name.startswith("iPhone"): - continue - candidates.append((name, udid)) - - candidates.sort(key=lambda it: (0 if "iPhone 16" in it[0] else 1, it[0])) - if candidates: - print(candidates[0][1]) - sys.exit(0) - - # Otherwise, create one from the newest available iOS runtime. - runtimes = json.loads(sh(["xcrun", "simctl", "list", "runtimes", "-j"])).get("runtimes") or [] - ios = [rt for rt in runtimes if rt.get("platform") == "iOS" and rt.get("isAvailable")] - if not ios: - print("No available iOS runtimes found.", file=sys.stderr) - sys.exit(1) - - def version_key(rt: dict) -> tuple[int, ...]: - parts: list[int] = [] - for p in str(rt.get("version") or "0").split("."): - try: - parts.append(int(p)) - except ValueError: - parts.append(0) - return tuple(parts) - - ios.sort(key=version_key, reverse=True) - runtime = ios[0] - runtime_id = str(runtime.get("identifier") or "") - if not runtime_id: - print("Missing iOS runtime identifier.", file=sys.stderr) - sys.exit(1) - - supported = runtime.get("supportedDeviceTypes") or [] - iphones = [dt for dt in supported if dt.get("productFamily") == "iPhone"] - if not iphones: - print("No iPhone device types for iOS runtime.", file=sys.stderr) - sys.exit(1) - - iphones.sort( - key=lambda dt: ( - 0 if "iPhone 16" in str(dt.get("name") or "") else 1, - str(dt.get("name") or ""), - ) - ) - device_type_id = str(iphones[0].get("identifier") or "") - if not device_type_id: - print("Missing iPhone device type identifier.", file=sys.stderr) - sys.exit(1) - - sim_name = f"CI iPhone {uuid.uuid4().hex[:8]}" - udid = sh(["xcrun", "simctl", "create", sim_name, device_type_id, runtime_id]) - if not udid: - print("Failed to create iPhone simulator.", file=sys.stderr) - sys.exit(1) - print(udid) - PY - )" - echo "Using iOS Simulator id: $DEST_ID" - xcodebuild test \ - -project apps/ios/Clawdis.xcodeproj \ - -scheme Clawdis \ - -destination "platform=iOS Simulator,id=$DEST_ID" \ - -resultBundlePath "$RESULT_BUNDLE_PATH" \ - -enableCodeCoverage YES - - - name: iOS coverage summary - run: | - set -euo pipefail - RESULT_BUNDLE_PATH="$RUNNER_TEMP/Clawdis-iOS.xcresult" - xcrun xccov view --report --only-targets "$RESULT_BUNDLE_PATH" - - - name: iOS coverage gate (43%) - run: | - set -euo pipefail - RESULT_BUNDLE_PATH="$RUNNER_TEMP/Clawdis-iOS.xcresult" - RESULT_BUNDLE_PATH="$RESULT_BUNDLE_PATH" python3 - <<'PY' - import json - import os - import subprocess - import sys - - target_name = "Clawdis.app" - minimum = 0.43 - - report = json.loads( - subprocess.check_output( - ["xcrun", "xccov", "view", "--report", "--json", os.environ["RESULT_BUNDLE_PATH"]], - text=True, - ) - ) - - target_coverage = None - for target in report.get("targets", []): - if target.get("name") == target_name: - target_coverage = float(target["lineCoverage"]) - break - - if target_coverage is None: - print(f"Could not find coverage for target: {target_name}") - sys.exit(1) - - print(f"{target_name} line coverage: {target_coverage * 100:.2f}% (min {minimum * 100:.2f}%)") - if target_coverage + 1e-12 < minimum: - sys.exit(1) - PY - - android: - needs: [docs-scope, changed-scope, check] - if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_android == 'true') - runs-on: blacksmith-4vcpu-ubuntu-2404 - strategy: - fail-fast: false - matrix: - include: - - task: test - command: ./gradlew --no-daemon :app:testDebugUnitTest - - task: build - command: ./gradlew --no-daemon :app:assembleDebug - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: false - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: 21 - - - name: Setup Android SDK - uses: android-actions/setup-android@v3 - with: - accept-android-sdk-licenses: false - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - with: - gradle-version: 8.11.1 - - - name: Install Android SDK packages - run: | - yes | sdkmanager --licenses >/dev/null - sdkmanager --install \ - "platform-tools" \ - "platforms;android-36" \ - "build-tools;36.0.0" - - - name: Run Android ${{ matrix.task }} - working-directory: apps/android + - name: Run tests (${{ matrix.runtime }}) run: ${{ matrix.command }} diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml deleted file mode 100644 index a286026ae32..00000000000 --- a/.github/workflows/docker-release.yml +++ /dev/null @@ -1,149 +0,0 @@ -name: Docker Release - -on: - push: - branches: - - main - tags: - - "v*" - paths-ignore: - - "docs/**" - - "**/*.md" - - "**/*.mdx" - - ".agents/**" - - "skills/**" - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - -jobs: - # Build amd64 image - build-amd64: - runs-on: ubuntu-latest - permissions: - packages: write - contents: read - outputs: - image-digest: ${{ steps.build.outputs.digest }} - image-metadata: ${{ steps.meta.outputs.json }} - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=ref,event=branch - type=semver,pattern={{version}} - type=semver,pattern={{version}},suffix=-amd64 - type=semver,pattern={{version}},suffix=-arm64 - type=ref,event=branch,suffix=-amd64 - type=ref,event=branch,suffix=-arm64 - - - name: Build and push amd64 image - id: build - uses: docker/build-push-action@v6 - with: - context: . - platforms: linux/amd64 - labels: ${{ steps.meta.outputs.labels }} - tags: ${{ steps.meta.outputs.tags }} - cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-cache:amd64 - cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-cache:amd64,mode=max - provenance: false - push: true - - # Build arm64 image - build-arm64: - runs-on: ubuntu-24.04-arm - permissions: - packages: write - contents: read - outputs: - image-digest: ${{ steps.build.outputs.digest }} - image-metadata: ${{ steps.meta.outputs.json }} - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=ref,event=branch - type=semver,pattern={{version}} - type=semver,pattern={{version}},suffix=-amd64 - type=semver,pattern={{version}},suffix=-arm64 - type=ref,event=branch,suffix=-amd64 - type=ref,event=branch,suffix=-arm64 - - - name: Build and push arm64 image - id: build - uses: docker/build-push-action@v6 - with: - context: . - platforms: linux/arm64 - labels: ${{ steps.meta.outputs.labels }} - tags: ${{ steps.meta.outputs.tags }} - cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-cache:arm64 - cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-cache:arm64,mode=max - provenance: false - push: true - - # Create multi-platform manifest - create-manifest: - runs-on: ubuntu-latest - permissions: - packages: write - contents: read - needs: [build-amd64, build-arm64] - steps: - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for manifest - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=ref,event=branch - type=semver,pattern={{version}} - - - name: Create and push manifest - run: | - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - ${{ needs.build-amd64.outputs.image-digest }} \ - ${{ needs.build-arm64.outputs.image-digest }} - env: - DOCKER_METADATA_OUTPUT_JSON: ${{ steps.meta.outputs.json }} diff --git a/.github/workflows/formal-conformance.yml b/.github/workflows/formal-conformance.yml deleted file mode 100644 index 8ba6d7e56b8..00000000000 --- a/.github/workflows/formal-conformance.yml +++ /dev/null @@ -1,139 +0,0 @@ -name: Formal models (informational conformance) - -on: - pull_request: - -concurrency: - group: formal-conformance-${{ github.event.pull_request.number || github.ref_name }} - cancel-in-progress: true - -jobs: - formal_conformance: - runs-on: ubuntu-latest - timeout-minutes: 20 - permissions: - contents: read - pull-requests: write - - steps: - - name: Checkout openclaw (PR) - uses: actions/checkout@v4 - with: - path: openclaw - - - name: Checkout formal models - uses: actions/checkout@v4 - with: - repository: vignesh07/clawdbot-formal-models - ref: main - path: clawdbot-formal-models - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: "22" - - - name: Regenerate extracted constants from openclaw - run: | - set -euo pipefail - cd clawdbot-formal-models - export OPENCLAW_REPO_DIR="${GITHUB_WORKSPACE}/openclaw" - node scripts/extract-tool-groups.mjs - node scripts/check-tool-group-alias.mjs - - # Drift is about extracted artifacts only; compute it before model checking - # to avoid any incidental file touches affecting the result. - - name: Compute drift (generated/*) - id: drift - run: | - set -euo pipefail - cd clawdbot-formal-models - - if git diff --quiet -- generated; then - echo "drift=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - - echo "drift=true" >> "$GITHUB_OUTPUT" - git diff -- generated > "${GITHUB_WORKSPACE}/formal-models-drift.diff" - - - name: Model check (green suite) - run: | - set -euo pipefail - cd clawdbot-formal-models - make \ - precedence groups elevated nodes-policy \ - attacker approvals approvals-token nodes-pipeline \ - gateway-exposure gateway-exposure-v2 gateway-exposure-v2-protected \ - gateway-auth-conformance gateway-auth-tailscale gateway-auth-proxy \ - pairing pairing-cap pairing-idempotency pairing-refresh pairing-refresh-race \ - ingress-gating ingress-idempotency ingress-dedupe-fallback ingress-trace ingress-trace2 \ - routing-isolation routing-precedence routing-identitylinks routing-identity-transitive routing-identity-symmetry routing-identity-channel-override \ - routing-thread-parent discord-pluralkit \ - ingress-retry session-key-stability session-explosion-bound config-normalization \ - queue-drain delivery-route-stability delivery-pipeline retry-termination retry-eventual-success \ - no-cross-stream multi-event-eventual-emission \ - dedupe-collision-fallback crash-restart-dedupe two-worker-dedupe openclaw-session-key-conformance \ - routing-thread-parent-channel-override routing-trirule gateway-auth-proxy-header-spoof \ - group-alias-check - - - name: Model check (negative suite, expected violations) - continue-on-error: true - run: | - set -euo pipefail - cd clawdbot-formal-models - make -k \ - precedence-negative groups-negative elevated-negative nodes-policy-negative \ - attacker-negative attacker-nodes-negative attacker-nodes-allowlist-negative attacker-nodes-allowlist-negative \ - approvals-negative approvals-token-negative nodes-pipeline-negative \ - gateway-exposure-negative gateway-exposure-v2-negative gateway-exposure-v2-protected-negative \ - gateway-exposure-v2-unsafe-custom gateway-exposure-v2-unsafe-tailnet gateway-exposure-v2-unsafe-auto \ - gateway-auth-conformance-negative gateway-auth-tailscale-negative gateway-auth-proxy-negative \ - pairing-negative pairing-cap-negative pairing-idempotency-negative pairing-refresh-negative pairing-refresh-race-negative \ - ingress-gating-negative ingress-idempotency-negative ingress-dedupe-fallback-negative ingress-trace-negative ingress-trace2-negative \ - routing-isolation-negative routing-precedence-negative routing-identitylinks-negative routing-identity-transitive-negative routing-identity-symmetry-negative routing-identity-channel-override-negative \ - routing-thread-parent-negative discord-pluralkit-negative \ - ingress-retry-negative session-key-stability-negative config-normalization-negative \ - queue-drain delivery-route-stability-negative delivery-pipeline-negative retry-termination-negative retry-eventual-success-negative \ - no-cross-stream-negative multi-event-eventual-emission-negative \ - dedupe-collision-fallback-negative crash-restart-dedupe-negative two-worker-dedupe-negative openclaw-session-key-conformance-negative \ - routing-thread-parent-channel-override-negative routing-trirule-negative gateway-auth-proxy-header-spoof-negative - - - name: Upload drift diff artifact - if: steps.drift.outputs.drift == 'true' - uses: actions/upload-artifact@v4 - with: - name: formal-models-conformance-drift - path: formal-models-drift.diff - - - name: Comment on PR (informational) - if: steps.drift.outputs.drift == 'true' - continue-on-error: true - uses: actions/github-script@v7 - with: - script: | - const body = [ - '⚠️ **Formal models conformance drift detected**', - '', - 'The formal models extracted constants (`generated/*`) do not match this openclaw PR.', - '', - 'This check is **informational** (not blocking merges yet).', - 'See the `formal-models-conformance-drift` artifact for the diff.', - '', - 'If this change is intentional, follow up by updating the formal models repo or regenerating the extracted artifacts there.', - ].join('\n'); - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.pull_request.number, - body, - }); - - - name: Summary - run: | - if [ "${{ steps.drift.outputs.drift }}" = "true" ]; then - echo "Formal conformance drift detected (informational)." - else - echo "Formal conformance: no drift." - fi diff --git a/.github/workflows/install-smoke.yml b/.github/workflows/install-smoke.yml deleted file mode 100644 index 61861a84be9..00000000000 --- a/.github/workflows/install-smoke.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Install Smoke - -on: - push: - branches: [main] - pull_request: - workflow_dispatch: - -concurrency: - group: install-smoke-${{ github.event.pull_request.number || github.sha }} - cancel-in-progress: true - -jobs: - docs-scope: - runs-on: ubuntu-latest - outputs: - docs_only: ${{ steps.check.outputs.docs_only }} - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Detect docs-only changes - id: check - uses: ./.github/actions/detect-docs-changes - - install-smoke: - needs: [docs-scope] - if: needs.docs-scope.outputs.docs_only != 'true' - runs-on: ubuntu-latest - steps: - - name: Checkout CLI - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22.x - check-latest: true - - - name: Setup pnpm + cache store - uses: ./.github/actions/setup-pnpm-store-cache - with: - pnpm-version: "10.23.0" - cache-key-suffix: "node22" - - - name: Install pnpm deps (minimal) - run: pnpm install --ignore-scripts --frozen-lockfile - - - name: Run installer docker tests - env: - CLAWDBOT_INSTALL_URL: https://openclaw.ai/install.sh - CLAWDBOT_INSTALL_CLI_URL: https://openclaw.ai/install-cli.sh - CLAWDBOT_NO_ONBOARD: "1" - CLAWDBOT_INSTALL_SMOKE_SKIP_CLI: "1" - CLAWDBOT_INSTALL_SMOKE_SKIP_NONROOT: ${{ github.event_name == 'pull_request' && '1' || '0' }} - CLAWDBOT_INSTALL_SMOKE_SKIP_PREVIOUS: "1" - run: pnpm test:install:smoke diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml deleted file mode 100644 index 2bae5a61160..00000000000 --- a/.github/workflows/labeler.yml +++ /dev/null @@ -1,519 +0,0 @@ -name: Labeler - -on: - pull_request_target: - types: [opened, synchronize, reopened] - issues: - types: [opened] - workflow_dispatch: - inputs: - max_prs: - description: "Maximum number of open PRs to process (0 = all)" - required: false - default: "200" - per_page: - description: "PRs per page (1-100)" - required: false - default: "50" - -permissions: {} - -jobs: - label: - permissions: - contents: read - pull-requests: write - runs-on: ubuntu-latest - steps: - - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 - id: app-token - with: - app-id: "2729701" - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5 - with: - configuration-path: .github/labeler.yml - repo-token: ${{ steps.app-token.outputs.token }} - sync-labels: true - - name: Apply PR size label - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 - with: - github-token: ${{ steps.app-token.outputs.token }} - script: | - const pullRequest = context.payload.pull_request; - if (!pullRequest) { - return; - } - - const sizeLabels = ["size: XS", "size: S", "size: M", "size: L", "size: XL"]; - const labelColor = "b76e79"; - - for (const label of sizeLabels) { - try { - await github.rest.issues.getLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: label, - }); - } catch (error) { - if (error?.status !== 404) { - throw error; - } - await github.rest.issues.createLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: label, - color: labelColor, - }); - } - } - - const files = await github.paginate(github.rest.pulls.listFiles, { - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: pullRequest.number, - per_page: 100, - }); - - const excludedLockfiles = new Set(["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"]); - const totalChangedLines = files.reduce((total, file) => { - const path = file.filename ?? ""; - if (path === "docs.acp.md" || path.startsWith("docs/") || excludedLockfiles.has(path)) { - return total; - } - return total + (file.additions ?? 0) + (file.deletions ?? 0); - }, 0); - - let targetSizeLabel = "size: XL"; - if (totalChangedLines < 50) { - targetSizeLabel = "size: XS"; - } else if (totalChangedLines < 200) { - targetSizeLabel = "size: S"; - } else if (totalChangedLines < 500) { - targetSizeLabel = "size: M"; - } else if (totalChangedLines < 1000) { - targetSizeLabel = "size: L"; - } - - const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pullRequest.number, - per_page: 100, - }); - - for (const label of currentLabels) { - const name = label.name ?? ""; - if (!sizeLabels.includes(name)) { - continue; - } - if (name === targetSizeLabel) { - continue; - } - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pullRequest.number, - name, - }); - } - - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pullRequest.number, - labels: [targetSizeLabel], - }); - - name: Apply maintainer or trusted-contributor label - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 - with: - github-token: ${{ steps.app-token.outputs.token }} - script: | - const login = context.payload.pull_request?.user?.login; - if (!login) { - return; - } - - const repo = `${context.repo.owner}/${context.repo.repo}`; - const trustedLabel = "trusted-contributor"; - const experiencedLabel = "experienced-contributor"; - const trustedThreshold = 4; - const experiencedThreshold = 10; - - let isMaintainer = false; - try { - const membership = await github.rest.teams.getMembershipForUserInOrg({ - org: context.repo.owner, - team_slug: "maintainer", - username: login, - }); - isMaintainer = membership?.data?.state === "active"; - } catch (error) { - if (error?.status !== 404) { - throw error; - } - } - - if (isMaintainer) { - await github.rest.issues.addLabels({ - ...context.repo, - issue_number: context.payload.pull_request.number, - labels: ["maintainer"], - }); - return; - } - - const mergedQuery = `repo:${repo} is:pr is:merged author:${login}`; - let mergedCount = 0; - try { - const merged = await github.rest.search.issuesAndPullRequests({ - q: mergedQuery, - per_page: 1, - }); - mergedCount = merged?.data?.total_count ?? 0; - } catch (error) { - if (error?.status !== 422) { - throw error; - } - core.warning(`Skipping merged search for ${login}; treating as 0.`); - } - - if (mergedCount >= experiencedThreshold) { - await github.rest.issues.addLabels({ - ...context.repo, - issue_number: context.payload.pull_request.number, - labels: [experiencedLabel], - }); - return; - } - - if (mergedCount >= trustedThreshold) { - await github.rest.issues.addLabels({ - ...context.repo, - issue_number: context.payload.pull_request.number, - labels: [trustedLabel], - }); - } - - backfill-pr-labels: - if: github.event_name == 'workflow_dispatch' - permissions: - contents: read - pull-requests: write - runs-on: ubuntu-latest - steps: - - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 - id: app-token - with: - app-id: "2729701" - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - name: Backfill PR labels - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 - with: - github-token: ${{ steps.app-token.outputs.token }} - script: | - const owner = context.repo.owner; - const repo = context.repo.repo; - const repoFull = `${owner}/${repo}`; - const inputs = context.payload.inputs ?? {}; - const maxPrsInput = inputs.max_prs ?? "200"; - const perPageInput = inputs.per_page ?? "50"; - const parsedMaxPrs = Number.parseInt(maxPrsInput, 10); - const parsedPerPage = Number.parseInt(perPageInput, 10); - const maxPrs = Number.isFinite(parsedMaxPrs) ? parsedMaxPrs : 200; - const perPage = Number.isFinite(parsedPerPage) ? Math.min(100, Math.max(1, parsedPerPage)) : 50; - const processAll = maxPrs <= 0; - const maxCount = processAll ? Number.POSITIVE_INFINITY : Math.max(1, maxPrs); - - const sizeLabels = ["size: XS", "size: S", "size: M", "size: L", "size: XL"]; - const labelColor = "b76e79"; - const trustedLabel = "trusted-contributor"; - const experiencedLabel = "experienced-contributor"; - const trustedThreshold = 4; - const experiencedThreshold = 10; - - const contributorCache = new Map(); - - async function ensureSizeLabels() { - for (const label of sizeLabels) { - try { - await github.rest.issues.getLabel({ - owner, - repo, - name: label, - }); - } catch (error) { - if (error?.status !== 404) { - throw error; - } - await github.rest.issues.createLabel({ - owner, - repo, - name: label, - color: labelColor, - }); - } - } - } - - async function resolveContributorLabel(login) { - if (contributorCache.has(login)) { - return contributorCache.get(login); - } - - let isMaintainer = false; - try { - const membership = await github.rest.teams.getMembershipForUserInOrg({ - org: owner, - team_slug: "maintainer", - username: login, - }); - isMaintainer = membership?.data?.state === "active"; - } catch (error) { - if (error?.status !== 404) { - throw error; - } - } - - if (isMaintainer) { - contributorCache.set(login, "maintainer"); - return "maintainer"; - } - - const mergedQuery = `repo:${repoFull} is:pr is:merged author:${login}`; - let mergedCount = 0; - try { - const merged = await github.rest.search.issuesAndPullRequests({ - q: mergedQuery, - per_page: 1, - }); - mergedCount = merged?.data?.total_count ?? 0; - } catch (error) { - if (error?.status !== 422) { - throw error; - } - core.warning(`Skipping merged search for ${login}; treating as 0.`); - } - - let label = null; - if (mergedCount >= experiencedThreshold) { - label = experiencedLabel; - } else if (mergedCount >= trustedThreshold) { - label = trustedLabel; - } - - contributorCache.set(login, label); - return label; - } - - async function applySizeLabel(pullRequest, currentLabels, labelNames) { - const files = await github.paginate(github.rest.pulls.listFiles, { - owner, - repo, - pull_number: pullRequest.number, - per_page: 100, - }); - - const excludedLockfiles = new Set(["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"]); - const totalChangedLines = files.reduce((total, file) => { - const path = file.filename ?? ""; - if (path === "docs.acp.md" || path.startsWith("docs/") || excludedLockfiles.has(path)) { - return total; - } - return total + (file.additions ?? 0) + (file.deletions ?? 0); - }, 0); - - let targetSizeLabel = "size: XL"; - if (totalChangedLines < 50) { - targetSizeLabel = "size: XS"; - } else if (totalChangedLines < 200) { - targetSizeLabel = "size: S"; - } else if (totalChangedLines < 500) { - targetSizeLabel = "size: M"; - } else if (totalChangedLines < 1000) { - targetSizeLabel = "size: L"; - } - - for (const label of currentLabels) { - const name = label.name ?? ""; - if (!sizeLabels.includes(name)) { - continue; - } - if (name === targetSizeLabel) { - continue; - } - await github.rest.issues.removeLabel({ - owner, - repo, - issue_number: pullRequest.number, - name, - }); - labelNames.delete(name); - } - - if (!labelNames.has(targetSizeLabel)) { - await github.rest.issues.addLabels({ - owner, - repo, - issue_number: pullRequest.number, - labels: [targetSizeLabel], - }); - labelNames.add(targetSizeLabel); - } - } - - async function applyContributorLabel(pullRequest, labelNames) { - const login = pullRequest.user?.login; - if (!login) { - return; - } - - const label = await resolveContributorLabel(login); - if (!label) { - return; - } - - if (labelNames.has(label)) { - return; - } - - await github.rest.issues.addLabels({ - owner, - repo, - issue_number: pullRequest.number, - labels: [label], - }); - labelNames.add(label); - } - - await ensureSizeLabels(); - - let page = 1; - let processed = 0; - - while (processed < maxCount) { - const remaining = maxCount - processed; - const pageSize = processAll ? perPage : Math.min(perPage, remaining); - const { data: pullRequests } = await github.rest.pulls.list({ - owner, - repo, - state: "open", - per_page: pageSize, - page, - }); - - if (pullRequests.length === 0) { - break; - } - - for (const pullRequest of pullRequests) { - if (!processAll && processed >= maxCount) { - break; - } - - const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, { - owner, - repo, - issue_number: pullRequest.number, - per_page: 100, - }); - - const labelNames = new Set( - currentLabels.map((label) => label.name).filter((name) => typeof name === "string"), - ); - - await applySizeLabel(pullRequest, currentLabels, labelNames); - await applyContributorLabel(pullRequest, labelNames); - - processed += 1; - } - - if (pullRequests.length < pageSize) { - break; - } - - page += 1; - } - - core.info(`Processed ${processed} pull requests.`); - - label-issues: - permissions: - issues: write - runs-on: ubuntu-latest - steps: - - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 - id: app-token - with: - app-id: "2729701" - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - name: Apply maintainer or trusted-contributor label - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 - with: - github-token: ${{ steps.app-token.outputs.token }} - script: | - const login = context.payload.issue?.user?.login; - if (!login) { - return; - } - - const repo = `${context.repo.owner}/${context.repo.repo}`; - const trustedLabel = "trusted-contributor"; - const experiencedLabel = "experienced-contributor"; - const trustedThreshold = 4; - const experiencedThreshold = 10; - - let isMaintainer = false; - try { - const membership = await github.rest.teams.getMembershipForUserInOrg({ - org: context.repo.owner, - team_slug: "maintainer", - username: login, - }); - isMaintainer = membership?.data?.state === "active"; - } catch (error) { - if (error?.status !== 404) { - throw error; - } - } - - if (isMaintainer) { - await github.rest.issues.addLabels({ - ...context.repo, - issue_number: context.payload.issue.number, - labels: ["maintainer"], - }); - return; - } - - const mergedQuery = `repo:${repo} is:pr is:merged author:${login}`; - let mergedCount = 0; - try { - const merged = await github.rest.search.issuesAndPullRequests({ - q: mergedQuery, - per_page: 1, - }); - mergedCount = merged?.data?.total_count ?? 0; - } catch (error) { - if (error?.status !== 422) { - throw error; - } - core.warning(`Skipping merged search for ${login}; treating as 0.`); - } - - if (mergedCount >= experiencedThreshold) { - await github.rest.issues.addLabels({ - ...context.repo, - issue_number: context.payload.issue.number, - labels: [experiencedLabel], - }); - return; - } - - if (mergedCount >= trustedThreshold) { - await github.rest.issues.addLabels({ - ...context.repo, - issue_number: context.payload.issue.number, - labels: [trustedLabel], - }); - } diff --git a/.github/workflows/sandbox-common-smoke.yml b/.github/workflows/sandbox-common-smoke.yml deleted file mode 100644 index 27c18aea572..00000000000 --- a/.github/workflows/sandbox-common-smoke.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Sandbox Common Smoke - -on: - push: - branches: [main] - paths: - - Dockerfile.sandbox - - Dockerfile.sandbox-common - - scripts/sandbox-common-setup.sh - pull_request: - paths: - - Dockerfile.sandbox - - Dockerfile.sandbox-common - - scripts/sandbox-common-setup.sh - -concurrency: - group: sandbox-common-smoke-${{ github.event.pull_request.number || github.sha }} - cancel-in-progress: true - -jobs: - sandbox-common-smoke: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: false - - - name: Build minimal sandbox base (USER sandbox) - shell: bash - run: | - set -euo pipefail - - docker build -t openclaw-sandbox-smoke-base:bookworm-slim - <<'EOF' - FROM debian:bookworm-slim - RUN useradd --create-home --shell /bin/bash sandbox - USER sandbox - WORKDIR /home/sandbox - EOF - - - name: Build sandbox-common image (root for installs, sandbox at runtime) - shell: bash - run: | - set -euo pipefail - - BASE_IMAGE="openclaw-sandbox-smoke-base:bookworm-slim" \ - TARGET_IMAGE="openclaw-sandbox-common-smoke:bookworm-slim" \ - PACKAGES="ca-certificates" \ - INSTALL_PNPM=0 \ - INSTALL_BUN=0 \ - INSTALL_BREW=0 \ - FINAL_USER=sandbox \ - scripts/sandbox-common-setup.sh - - u="$(docker run --rm openclaw-sandbox-common-smoke:bookworm-slim sh -lc 'id -un')" - test "$u" = "sandbox" diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml deleted file mode 100644 index ccafcf01a18..00000000000 --- a/.github/workflows/stale.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Stale - -on: - schedule: - - cron: "17 3 * * *" - workflow_dispatch: - -permissions: {} - -jobs: - stale: - permissions: - issues: write - pull-requests: write - runs-on: ubuntu-latest - steps: - - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 - id: app-token - with: - app-id: "2729701" - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - name: Mark stale issues and pull requests - uses: actions/stale@v9 - with: - repo-token: ${{ steps.app-token.outputs.token }} - 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 - exempt-pr-labels: maintainer,no-stale - operations-per-run: 500 - exempt-all-assignees: true - remove-stale-when-updated: true - stale-issue-message: | - This issue has been automatically marked as stale due to inactivity. - Please add updates or it will be closed. - stale-pr-message: | - This pull request has been automatically marked as stale due to inactivity. - Please add updates or it will be closed. - close-issue-message: | - 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 repro steps. - close-issue-reason: not_planned - close-pr-message: | - Closing due to inactivity. - 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.