mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-22 07:11:39 +00:00
fix(ci): stop unrelated main updates from forcing PR rebases (#107050)
* fix(ci): stop unrelated main updates from forcing PR rebases * docs(ci): document reusable aggregate proof * docs(ci): clarify immutable gate success * chore(ci): refresh stalled PR head
This commit is contained in:
committed by
GitHub
parent
b58a75d726
commit
606e2f5dba
93
.github/workflows/ci.yml
vendored
93
.github/workflows/ci.yml
vendored
@@ -50,8 +50,6 @@ on:
|
||||
- "docs/**"
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize, ready_for_review, converted_to_draft]
|
||||
paths-ignore:
|
||||
- "CHANGELOG.md"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -2777,6 +2775,97 @@ jobs:
|
||||
;;
|
||||
esac
|
||||
|
||||
ci-gate:
|
||||
permissions:
|
||||
contents: read
|
||||
name: openclaw/ci-gate
|
||||
needs:
|
||||
- runner-admission
|
||||
- preflight
|
||||
- security-fast
|
||||
- pnpm-store-warmup
|
||||
- build-artifacts
|
||||
- native-i18n
|
||||
- checks-ui
|
||||
- control-ui-i18n
|
||||
- checks-fast-core
|
||||
- qa-smoke-ci-profile
|
||||
- checks-fast-plugin-contracts-shard
|
||||
- checks-fast-channel-contracts-shard
|
||||
- checks-node-compat
|
||||
- checks-node-core-test-nondist-shard
|
||||
- check-shard
|
||||
- check-additional-shard
|
||||
- check-docs
|
||||
- skills-python
|
||||
- checks-windows
|
||||
- macos-node
|
||||
- macos-swift
|
||||
- ios-build
|
||||
- android
|
||||
if: ${{ always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Verify selected CI lanes
|
||||
shell: bash
|
||||
env:
|
||||
REQUIRED_RESULTS: |
|
||||
runner-admission=${{ needs.runner-admission.result }}
|
||||
preflight=${{ needs.preflight.result }}
|
||||
security-fast=${{ needs.security-fast.result }}
|
||||
SELECTED_RESULTS: |
|
||||
pnpm-store-warmup=${{ needs.pnpm-store-warmup.result }}
|
||||
build-artifacts=${{ needs.build-artifacts.result }}
|
||||
native-i18n=${{ needs.native-i18n.result }}
|
||||
checks-ui=${{ needs.checks-ui.result }}
|
||||
control-ui-i18n=${{ needs.control-ui-i18n.result }}
|
||||
checks-fast-core=${{ needs.checks-fast-core.result }}
|
||||
qa-smoke-ci-profile=${{ needs.qa-smoke-ci-profile.result }}
|
||||
checks-fast-plugin-contracts-shard=${{ needs.checks-fast-plugin-contracts-shard.result }}
|
||||
checks-fast-channel-contracts-shard=${{ needs.checks-fast-channel-contracts-shard.result }}
|
||||
checks-node-compat=${{ needs.checks-node-compat.result }}
|
||||
checks-node-core-test-nondist-shard=${{ needs.checks-node-core-test-nondist-shard.result }}
|
||||
check-shard=${{ needs.check-shard.result }}
|
||||
check-additional-shard=${{ needs.check-additional-shard.result }}
|
||||
check-docs=${{ needs.check-docs.result }}
|
||||
skills-python=${{ needs.skills-python.result }}
|
||||
checks-windows=${{ needs.checks-windows.result }}
|
||||
macos-node=${{ needs.macos-node.result }}
|
||||
macos-swift=${{ needs.macos-swift.result }}
|
||||
ios-build=${{ needs.ios-build.result }}
|
||||
android=${{ needs.android.result }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
failures=0
|
||||
while IFS= read -r entry; do
|
||||
[[ -n "$entry" ]] || continue
|
||||
name="${entry%%=*}"
|
||||
result="${entry#*=}"
|
||||
echo "${name}: ${result}"
|
||||
if [[ "$result" != "success" ]]; then
|
||||
echo "::error title=Required CI job did not succeed::${name} finished with ${result}"
|
||||
failures=1
|
||||
fi
|
||||
done <<< "$REQUIRED_RESULTS"
|
||||
|
||||
while IFS= read -r entry; do
|
||||
[[ -n "$entry" ]] || continue
|
||||
name="${entry%%=*}"
|
||||
result="${entry#*=}"
|
||||
echo "${name}: ${result}"
|
||||
case "$result" in
|
||||
success | skipped) ;;
|
||||
*)
|
||||
echo "::error title=Selected CI job did not succeed::${name} finished with ${result}"
|
||||
failures=1
|
||||
;;
|
||||
esac
|
||||
done <<< "$SELECTED_RESULTS"
|
||||
|
||||
exit "$failures"
|
||||
|
||||
ci-timings-summary:
|
||||
permissions:
|
||||
actions: read
|
||||
|
||||
13
docs/ci.md
13
docs/ci.md
@@ -9,8 +9,8 @@ read_when:
|
||||
---
|
||||
|
||||
OpenClaw CI runs on pushes to `main` (Markdown and `docs/**` paths are ignored
|
||||
at the trigger), on non-draft pull requests (CHANGELOG-only diffs are ignored),
|
||||
and on manual dispatch. Canonical `main` pushes first pass through a 90-second
|
||||
at the trigger), on every non-draft pull request, and on manual dispatch.
|
||||
Canonical `main` pushes first pass through a 90-second
|
||||
hosted-runner admission window; the `CI` concurrency group cancels that waiting
|
||||
run when a newer commit lands, so sequential merges do not each register a full
|
||||
Blacksmith matrix. Pull requests and manual dispatches skip the wait. The
|
||||
@@ -49,6 +49,7 @@ dispatch.
|
||||
| `macos-swift` | Swift lint, build, and tests for the macOS app | macOS-relevant changes |
|
||||
| `ios-build` | Xcode project generation plus the iOS app simulator build | iOS app, shared app kit, or Swabble changes |
|
||||
| `android` | Android unit tests for both flavors plus one debug APK build | Android-relevant changes |
|
||||
| `openclaw/ci-gate` | Final aggregate: requires admission, preflight, and security; accepts skips only for manifest-disabled downstream lanes | Every non-draft CI run |
|
||||
| `test-performance-agent` | Separate workflow: daily Codex slow-test optimization after trusted activity | Main CI success or manual dispatch |
|
||||
| `openclaw-performance` | Separate workflow: daily/on-demand Kova runtime performance reports with mock-provider, deep-profile, and GPT 5.6 live lanes | Scheduled and manual dispatch |
|
||||
|
||||
@@ -61,6 +62,14 @@ Standalone Periphery workflows enforce zero dead-code findings for the iOS and m
|
||||
3. `security-fast`, `check-*`, `check-additional-*`, `check-docs`, and `skills-python` fail quickly without waiting on the heavier artifact and platform matrix jobs.
|
||||
4. `build-artifacts` and the advisory `control-ui-i18n` check overlap with the fast Linux lanes. Generated locale drift stays visible while the standalone refresh workflow repairs it in the background.
|
||||
5. Heavier platform and runtime lanes fan out after that: `checks-fast-core`, `checks-fast-contracts-plugins-*`, `checks-fast-contracts-channels-*`, `checks-node-*`, `checks-windows`, `macos-node`, `macos-swift`, `ios-build`, and `android`.
|
||||
6. `openclaw/ci-gate` waits for every selected lane. Admission, preflight, and security must succeed; downstream jobs may skip only when the manifest did not select them. A failed or canceled selected lane fails the aggregate.
|
||||
|
||||
The merge coordinator may reuse an authenticated successful `openclaw/ci-gate`
|
||||
for the same pull-request head for up to 24 hours. This avoids rewriting a
|
||||
contributor branch after unrelated `main` changes. The reusable result does not
|
||||
replace the separate strict, App-owned test-merge check against current `main`.
|
||||
A later pending or failed rerun does not erase an earlier successful result for
|
||||
that unchanged head during the freshness window.
|
||||
|
||||
GitHub may mark superseded jobs as `cancelled` when a newer push lands on the same PR or `main` ref. Treat that as CI noise unless the newest run for the same ref is also failing. Matrix jobs use `fail-fast: false`, and `build-artifacts` reports embedded channel, core-support-boundary, and gateway-watch failures directly instead of queuing tiny verifier jobs. The automatic CI concurrency key is versioned (`CI-v7-*`) so a GitHub-side zombie in an old queue group cannot indefinitely block newer main runs. Manual full-suite runs use `CI-manual-v1-*` and do not cancel in-progress runs. The plugin-list startup-memory guard keeps a 350 MiB ceiling on self-hosted Blacksmith Linux and allows 425 MiB on GitHub-hosted Linux, whose RSS baseline is higher for the same built CLI.
|
||||
|
||||
|
||||
@@ -53,6 +53,20 @@ function readCiWorkflow() {
|
||||
return parse(readFileSync(".github/workflows/ci.yml", "utf8"));
|
||||
}
|
||||
|
||||
function runCiGateFixture(requiredResults: string, selectedResults: string) {
|
||||
const gateStep = readCiWorkflow().jobs["ci-gate"].steps.find(
|
||||
(step: WorkflowStep) => step.name === "Verify selected CI lanes",
|
||||
);
|
||||
return spawnSync("bash", ["-c", gateStep.run], {
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
REQUIRED_RESULTS: requiredResults,
|
||||
SELECTED_RESULTS: selectedResults,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function runCiManifestFixture(options: {
|
||||
bundledPlanner: boolean;
|
||||
changedPlannerImportFails?: boolean;
|
||||
@@ -2677,6 +2691,91 @@ describe("ci workflow guards", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("emits one final CI gate after every selected lane", () => {
|
||||
const workflow = readCiWorkflow();
|
||||
const gate = workflow.jobs["ci-gate"];
|
||||
const requiredJobs = ["runner-admission", "preflight", "security-fast"];
|
||||
const selectedJobs = [
|
||||
"pnpm-store-warmup",
|
||||
"build-artifacts",
|
||||
"native-i18n",
|
||||
"checks-ui",
|
||||
"control-ui-i18n",
|
||||
"checks-fast-core",
|
||||
"qa-smoke-ci-profile",
|
||||
"checks-fast-plugin-contracts-shard",
|
||||
"checks-fast-channel-contracts-shard",
|
||||
"checks-node-compat",
|
||||
"checks-node-core-test-nondist-shard",
|
||||
"check-shard",
|
||||
"check-additional-shard",
|
||||
"check-docs",
|
||||
"skills-python",
|
||||
"checks-windows",
|
||||
"macos-node",
|
||||
"macos-swift",
|
||||
"ios-build",
|
||||
"android",
|
||||
];
|
||||
|
||||
expect(workflow.on.pull_request).not.toHaveProperty("paths-ignore");
|
||||
expect(gate.name).toBe("openclaw/ci-gate");
|
||||
expect(gate.needs).toEqual([...requiredJobs, ...selectedJobs]);
|
||||
expect(gate.needs.toSorted()).toEqual(
|
||||
Object.keys(workflow.jobs)
|
||||
.filter((job) => job !== "ci-gate" && job !== "ci-timings-summary")
|
||||
.toSorted(),
|
||||
);
|
||||
expect(gate.if).toBe(
|
||||
"${{ always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }}",
|
||||
);
|
||||
expect(gate["runs-on"]).toBe("ubuntu-24.04");
|
||||
expect(gate.permissions).toEqual({ contents: "read" });
|
||||
|
||||
const verifyStep = gate.steps.find(
|
||||
(step: WorkflowStep) => step.name === "Verify selected CI lanes",
|
||||
);
|
||||
expect(Object.keys(verifyStep.env).toSorted()).toEqual([
|
||||
"REQUIRED_RESULTS",
|
||||
"SELECTED_RESULTS",
|
||||
]);
|
||||
for (const job of requiredJobs) {
|
||||
expect(verifyStep.env.REQUIRED_RESULTS).toContain(`${job}=\${{ needs.${job}.result }}`);
|
||||
}
|
||||
for (const job of selectedJobs) {
|
||||
expect(verifyStep.env.SELECTED_RESULTS).toContain(`${job}=\${{ needs.${job}.result }}`);
|
||||
}
|
||||
expect(verifyStep.run).toContain("Required CI job did not succeed");
|
||||
expect(verifyStep.run).toContain("success | skipped");
|
||||
expect(verifyStep.run).toContain("Selected CI job did not succeed");
|
||||
});
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"accepts only successful required jobs and successful or skipped selected jobs",
|
||||
() => {
|
||||
const passing = runCiGateFixture(
|
||||
"runner-admission=success\npreflight=success\nsecurity-fast=success",
|
||||
"checks-ui=success\nmacos-swift=skipped",
|
||||
);
|
||||
expect(passing.status, `${passing.stdout}\n${passing.stderr}`).toBe(0);
|
||||
|
||||
const skippedRequired = runCiGateFixture(
|
||||
"runner-admission=success\npreflight=skipped\nsecurity-fast=success",
|
||||
"checks-ui=skipped",
|
||||
);
|
||||
expect(skippedRequired.status).not.toBe(0);
|
||||
expect(skippedRequired.stdout).toContain("preflight finished with skipped");
|
||||
|
||||
const failedSelected = runCiGateFixture(
|
||||
"runner-admission=success\npreflight=success\nsecurity-fast=success",
|
||||
"checks-ui=failure\nmacos-swift=cancelled",
|
||||
);
|
||||
expect(failedSelected.status).not.toBe(0);
|
||||
expect(failedSelected.stdout).toContain("checks-ui finished with failure");
|
||||
expect(failedSelected.stdout).toContain("macos-swift finished with cancelled");
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps maturity scorecard generated QA evidence handoff strict", () => {
|
||||
const maturityWorkflow = readMaturityScorecardWorkflow();
|
||||
const qaEvidenceWorkflow = readQaProfileEvidenceWorkflow();
|
||||
|
||||
Reference in New Issue
Block a user