Skip to content

Commit 3e16818

Browse files
authored
Merge pull request #17 from Kilo-Org/ci/real-openclaw-install
feat(testing) integration testing with openclaw
2 parents da71f03 + c1a146e commit 3e16818

4 files changed

Lines changed: 199 additions & 0 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: install-preflight
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
12+
13+
# Cancel superseded runs on the same ref. Keeps resource use linear
14+
# with the number of open PRs, not the number of pushes per PR.
15+
concurrency:
16+
group: install-preflight-${{ github.ref }}
17+
cancel-in-progress: true
18+
19+
jobs:
20+
install-preflight:
21+
# Security: the job installs ~450 npm packages (~54 MB across 7,500
22+
# files) and takes a few minutes per run. On a public repo that
23+
# makes it an attractive DoS vector via fork PRs. Skip the job
24+
# when the PR originates from a fork — a maintainer can instead
25+
# run `bun run install-preflight` locally against the fork's code
26+
# (or merge into a same-repo branch) to exercise the real check.
27+
# Same-repo branches and direct pushes are unaffected.
28+
if: >-
29+
github.event_name != 'pull_request' ||
30+
github.event.pull_request.head.repo.full_name == github.repository
31+
runs-on: ubuntu-latest
32+
# Hard wall-time cap so a hung npm install or stuck openclaw CLI
33+
# can't sit on a runner. Normal runs finish in ~1–2 minutes.
34+
timeout-minutes: 10
35+
steps:
36+
- uses: actions/checkout@v4
37+
38+
# Pin Node 24 (current LTS) because openclaw@latest requires Node
39+
# >=22.14 — GitHub's ubuntu-latest runners still ship Node 20 by
40+
# default, so without this step the openclaw CLI refuses to run
41+
# ("Node.js v22.12+ is required"). Node 24 also matches the
42+
# wider KiloCode ecosystem engines field and covers any near-term
43+
# openclaw bumps. bun has its own runtime and is independent.
44+
- uses: actions/setup-node@v4
45+
with:
46+
node-version: "24"
47+
48+
- uses: oven-sh/setup-bun@v2
49+
with:
50+
bun-version: latest
51+
52+
- name: Install dev dependencies
53+
run: bun install --frozen-lockfile
54+
55+
# Packs this plugin, installs openclaw@latest in a throwaway
56+
# dir, and runs `openclaw plugins install <tarball>` — the same
57+
# real path end users hit. Exits non-zero on any scanner /
58+
# denylist rejection. Never add bypass flags (--dangerously-
59+
# force-unsafe-install, --force, etc.) here or in the script; a
60+
# rejection means the plugin source needs fixing, not the check.
61+
- name: OpenClaw install preflight
62+
run: bun run install-preflight

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
## [Unreleased]
1010

11+
### Added
12+
13+
- CI `install-preflight` workflow that packs this plugin into its
14+
real publish shape (`bun pm pack`) and runs it through
15+
`openclaw plugins install <tarball>` against the current
16+
`openclaw@latest` from npm. Exercises the same install path an
17+
end user hits, including the real scanner and dependency denylist
18+
— no regex reimplementation and no force/bypass flags. Drift
19+
against upstream rule changes surfaces on the next PR automatically.
20+
Runs on push to `main`, pull requests, and manual dispatch.
21+
Locally reproducible via `bun run install-preflight`.
22+
1123
### Changed
1224

1325
- Extracted `resolveEnvToken()` and `resolveApiBase()` out of

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"typecheck": "tsc --noEmit",
3535
"format": "prettier --write \"**/*.{ts,json,md,yml}\"",
3636
"format:check": "prettier --check \"**/*.{ts,json,md,yml}\"",
37+
"install-preflight": "bun run script/install-preflight.ts",
3738
"scan": "bun run script/scan.ts",
3839
"test": "bun test"
3940
},

script/install-preflight.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
#!/usr/bin/env bun
2+
3+
/**
4+
* CI preflight that runs @kilocode/shell-security through the REAL
5+
* OpenClaw install path — the same code path an end user hits when
6+
* they type `openclaw plugins install @kilocode/shell-security`.
7+
*
8+
* Steps:
9+
* 1. Pack this plugin into a tarball (strips `private: true` the
10+
* same way script/publish.ts does, then restores it).
11+
* 2. Install `openclaw@latest` into a throwaway node_modules.
12+
* 3. Run `openclaw plugins install <tarball>` against a throwaway
13+
* OPENCLAW_STATE_DIR.
14+
* 4. Exit with whatever exit code `openclaw plugins install` gave
15+
* us. Non-zero = scanner/denylist rejection = CI fails.
16+
*
17+
* NO bypass flags. Never add `--dangerously-force-unsafe-install`,
18+
* `--force`, `--skip-scan`, or any other "ignore the safety gate"
19+
* option to this script. The whole point is to exercise the real
20+
* gate. If the gate rejects the plugin, the fix is in the plugin
21+
* source (see src/env.ts for the project's env-isolation convention)
22+
* — never in this script.
23+
*
24+
* `OPENCLAW_DISABLE_BUNDLED_PLUGIN_POSTINSTALL=1` below is NOT a
25+
* bypass flag. It skips openclaw's postinstall step that sets up
26+
* compat sidecars for bundled channel plugins (Telegram, Slack,
27+
* Matrix, etc.) used by the gateway itself — none of which is
28+
* relevant to `plugins install`. The scanner code path this script
29+
* exercises is unaffected. Removing it just makes CI slower.
30+
*
31+
* Run locally: `bun run install-preflight`
32+
* CI: `.github/workflows/install-preflight.yml`
33+
*/
34+
35+
import { $ } from "bun";
36+
import fs from "node:fs/promises";
37+
import os from "node:os";
38+
import path from "node:path";
39+
import { fileURLToPath } from "node:url";
40+
41+
const repoRoot = fileURLToPath(new URL("..", import.meta.url));
42+
process.chdir(repoRoot);
43+
44+
const pkgPath = path.join(repoRoot, "package.json");
45+
const originalPkgText = await fs.readFile(pkgPath, "utf8");
46+
47+
// The finally block restores package.json and cleans up artifacts. It
48+
// is critical that ANY mutation of pkgPath happens inside the try so
49+
// the restore is always armed, and that we NEVER call `process.exit()`
50+
// inside the try — that would terminate the process immediately and
51+
// skip finally, leaving `private: true` stripped permanently (which
52+
// is exactly the publish-safety regression this script must not cause).
53+
// Instead we set `process.exitCode` at the bottom and let the event
54+
// loop drain normally.
55+
let tarball: string | undefined;
56+
let ciTmp: string | undefined;
57+
let openclawExit = 0;
58+
try {
59+
// --- Pack --------------------------------------------------------
60+
const pkg = JSON.parse(originalPkgText);
61+
delete pkg.private;
62+
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
63+
64+
// `bun pm pack --quiet` emits only the tarball filename.
65+
tarball = (await $`bun pm pack --quiet`.text()).trim();
66+
if (!tarball) throw new Error("bun pm pack produced no tarball");
67+
console.log(`Packed: ${tarball}`);
68+
69+
// --- Install openclaw --------------------------------------------
70+
ciTmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-preflight-"));
71+
const stateDir = path.join(ciTmp, "state");
72+
const nodeRoot = path.join(ciTmp, "node");
73+
await fs.mkdir(stateDir, { recursive: true });
74+
await fs.mkdir(nodeRoot, { recursive: true });
75+
76+
const latest = (await $`npm view openclaw dist-tags.latest`.text()).trim();
77+
console.log(`Resolved openclaw@${latest}`);
78+
79+
// Minimal scratch package so npm has somewhere to land node_modules.
80+
await fs.writeFile(
81+
path.join(nodeRoot, "package.json"),
82+
JSON.stringify(
83+
{ name: "openclaw-preflight-scratch", private: true },
84+
null,
85+
2,
86+
) + "\n",
87+
);
88+
89+
process.env.OPENCLAW_DISABLE_BUNDLED_PLUGIN_POSTINSTALL = "1";
90+
await $`npm install --prefix ${nodeRoot} --no-save openclaw@${latest}`;
91+
92+
// --- Run the real install ----------------------------------------
93+
process.env.OPENCLAW_STATE_DIR = stateDir;
94+
const tarballAbs = path.resolve(repoRoot, tarball);
95+
const cli = path.join(nodeRoot, "node_modules", ".bin", "openclaw");
96+
console.log(`Running: ${cli} plugins install ${tarballAbs}`);
97+
const result = await $`${cli} plugins install ${tarballAbs}`.nothrow();
98+
99+
if (result.exitCode !== 0) {
100+
console.error(
101+
`\nFAIL: openclaw plugins install exited ${result.exitCode}. See output above.`,
102+
);
103+
console.error(
104+
"If this is an env-harvesting or other scanner rejection, the fix is in the plugin source (see src/env.ts for the project's env-isolation convention). Do NOT add a bypass flag to this script.",
105+
);
106+
openclawExit = result.exitCode;
107+
} else {
108+
console.log("\nOK: openclaw plugins install succeeded");
109+
}
110+
} finally {
111+
// Always restore package.json (restores `private: true`), always
112+
// clean up the tarball, always clean up the scratch openclaw install
113+
// dir so repeated local runs don't accumulate ~400MB of node_modules
114+
// under os.tmpdir().
115+
await fs.writeFile(pkgPath, originalPkgText);
116+
if (tarball) {
117+
await $`rm -f ${tarball}`.nothrow();
118+
}
119+
if (ciTmp) {
120+
await $`rm -rf ${ciTmp}`.nothrow();
121+
}
122+
}
123+
124+
process.exitCode = openclawExit;

0 commit comments

Comments
 (0)