mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(ci): scope PR merge diff checks to first parent (#90287)
Summary: - This PR adds opt-in first-parent merge-head diff-base handling for CI changed-scope, changed-lanes, and OpenGrep PR scans, plus synthetic merge coverage and small lint/type cleanups. - PR surface: Source +6, Tests +204, Config +1, Other +179. Total +390 across 15 files. - Reproducibility: yes. The synthetic merge tests and PR body live-ref proof show the stale payload-base path can include main-only files, and first-parent mode narrows it to PR-owned paths. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(ci): update workflow guard expectations - PR branch already contained follow-up commit before automerge: fix(ci): resolve plugin guardrail lint failures - PR branch already contained follow-up commit before automerge: fix(ci): preserve plugin run context typing - PR branch already contained follow-up commit before automerge: fix(ci): scope PR merge diff checks to first parent Validation: - ClawSweeper review passed for head40235e8c3d. - Required merge gates passed before the squash merge. Prepared head SHA:40235e8c3dReview: https://github.com/openclaw/openclaw/pull/90287#issuecomment-4621155576 Co-authored-by: Mason Huang <masonxhuang@tencent.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: hxy91819 Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
This commit is contained in:
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -92,7 +92,7 @@ jobs:
|
|||||||
for attempt in 1 2 3; do
|
for attempt in 1 2 3; do
|
||||||
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
|
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
|
||||||
-c protocol.version=2 \
|
-c protocol.version=2 \
|
||||||
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
|
fetch --no-tags --prune --no-recurse-submodules --depth=2 origin \
|
||||||
"+${ref}:refs/remotes/origin/checkout" && return 0
|
"+${ref}:refs/remotes/origin/checkout" && return 0
|
||||||
fetch_status="$?"
|
fetch_status="$?"
|
||||||
if [ "$fetch_status" != "124" ] && [ "$fetch_status" != "137" ]; then
|
if [ "$fetch_status" != "124" ] && [ "$fetch_status" != "137" ]; then
|
||||||
@@ -146,12 +146,12 @@ jobs:
|
|||||||
|
|
||||||
if [ "${{ github.event_name }}" = "push" ]; then
|
if [ "${{ github.event_name }}" = "push" ]; then
|
||||||
BASE="${{ github.event.before }}"
|
BASE="${{ github.event.before }}"
|
||||||
|
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD
|
||||||
else
|
else
|
||||||
BASE="${{ github.event.pull_request.base.sha }}"
|
BASE="${{ github.event.pull_request.base.sha }}"
|
||||||
|
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD --merge-head-first-parent
|
||||||
fi
|
fi
|
||||||
|
|
||||||
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD
|
|
||||||
|
|
||||||
- name: Build CI manifest
|
- name: Build CI manifest
|
||||||
id: manifest
|
id: manifest
|
||||||
env:
|
env:
|
||||||
|
|||||||
3
.github/workflows/opengrep-precise.yml
vendored
3
.github/workflows/opengrep-precise.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.sha }}
|
ref: ${{ github.sha }}
|
||||||
fetch-depth: 1
|
fetch-depth: 2
|
||||||
fetch-tags: false
|
fetch-tags: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: false
|
submodules: false
|
||||||
@@ -74,6 +74,7 @@ jobs:
|
|||||||
- name: Run opengrep on PR diff
|
- name: Run opengrep on PR diff
|
||||||
env:
|
env:
|
||||||
OPENCLAW_OPENGREP_BASE_REF: ${{ github.event.pull_request.base.sha }}...HEAD
|
OPENCLAW_OPENGREP_BASE_REF: ${{ github.event.pull_request.base.sha }}...HEAD
|
||||||
|
OPENCLAW_OPENGREP_MERGE_HEAD_FIRST_PARENT: "1"
|
||||||
# Findings from precise rules block this workflow. Pull requests scan
|
# Findings from precise rules block this workflow. Pull requests scan
|
||||||
# changed first-party source paths only so findings stay attributable to
|
# changed first-party source paths only so findings stay attributable to
|
||||||
# the PR diff. Test/fixture/QA path exclusions live in `.semgrepignore`
|
# the PR diff. Test/fixture/QA path exclusions live in `.semgrepignore`
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { execFileSync } from "node:child_process";
|
|||||||
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
||||||
import { booleanFlag, parseFlagArgs, stringFlag } from "./lib/arg-utils.mjs";
|
import { booleanFlag, parseFlagArgs, stringFlag } from "./lib/arg-utils.mjs";
|
||||||
import { isDirectRunUrl } from "./lib/direct-run.mjs";
|
import { isDirectRunUrl } from "./lib/direct-run.mjs";
|
||||||
|
import { resolveMergeHeadDiffBase } from "./lib/merge-head-diff-base.mjs";
|
||||||
|
|
||||||
const GIT_OUTPUT_MAX_BUFFER = 64 * 1024 * 1024;
|
const GIT_OUTPUT_MAX_BUFFER = 64 * 1024 * 1024;
|
||||||
const IMPLAUSIBLE_NO_MERGE_BASE_DIFF_PATHS = 200;
|
const IMPLAUSIBLE_NO_MERGE_BASE_DIFF_PATHS = 200;
|
||||||
@@ -213,13 +214,21 @@ export function detectChangedLanes(changedPaths, options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{ paths: string[]; base: string; head?: string; staged?: boolean }} params
|
* @param {{ paths: string[]; base: string; head?: string; staged?: boolean; mergeHeadFirstParent?: boolean }} params
|
||||||
* @returns {ChangedLaneResult}
|
* @returns {ChangedLaneResult}
|
||||||
*/
|
*/
|
||||||
export function detectChangedLanesForPaths(params) {
|
export function detectChangedLanesForPaths(params) {
|
||||||
|
const base = params.staged
|
||||||
|
? params.base
|
||||||
|
: resolveMergeHeadDiffBase({
|
||||||
|
base: params.base,
|
||||||
|
head: params.head ?? "HEAD",
|
||||||
|
maxBuffer: GIT_OUTPUT_MAX_BUFFER,
|
||||||
|
preferFirstParent: params.mergeHeadFirstParent === true,
|
||||||
|
});
|
||||||
const packageJsonChangeKind = params.paths.includes("package.json")
|
const packageJsonChangeKind = params.paths.includes("package.json")
|
||||||
? classifyPackageJsonChangeFromGit({
|
? classifyPackageJsonChangeFromGit({
|
||||||
base: params.base,
|
base,
|
||||||
head: params.head,
|
head: params.head,
|
||||||
staged: params.staged,
|
staged: params.staged,
|
||||||
})
|
})
|
||||||
@@ -228,13 +237,19 @@ export function detectChangedLanesForPaths(params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{ base: string; head?: string; includeWorktree?: boolean; cwd?: string }} params
|
* @param {{ base: string; head?: string; includeWorktree?: boolean; cwd?: string; mergeHeadFirstParent?: boolean }} params
|
||||||
* @returns {string[]}
|
* @returns {string[]}
|
||||||
*/
|
*/
|
||||||
export function listChangedPathsFromGit(params) {
|
export function listChangedPathsFromGit(params) {
|
||||||
const base = params.base;
|
|
||||||
const head = params.head ?? "HEAD";
|
const head = params.head ?? "HEAD";
|
||||||
const cwd = params.cwd ?? process.cwd();
|
const cwd = params.cwd ?? process.cwd();
|
||||||
|
const base = resolveMergeHeadDiffBase({
|
||||||
|
base: params.base,
|
||||||
|
head,
|
||||||
|
cwd,
|
||||||
|
maxBuffer: GIT_OUTPUT_MAX_BUFFER,
|
||||||
|
preferFirstParent: params.mergeHeadFirstParent === true,
|
||||||
|
});
|
||||||
if (!base) {
|
if (!base) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -453,6 +468,7 @@ function parseArgs(argv) {
|
|||||||
base: "origin/main",
|
base: "origin/main",
|
||||||
head: "HEAD",
|
head: "HEAD",
|
||||||
staged: false,
|
staged: false,
|
||||||
|
mergeHeadFirstParent: false,
|
||||||
json: false,
|
json: false,
|
||||||
githubOutput: false,
|
githubOutput: false,
|
||||||
help: false,
|
help: false,
|
||||||
@@ -465,6 +481,7 @@ function parseArgs(argv) {
|
|||||||
stringFlag("--base", "base"),
|
stringFlag("--base", "base"),
|
||||||
stringFlag("--head", "head"),
|
stringFlag("--head", "head"),
|
||||||
booleanFlag("--staged", "staged"),
|
booleanFlag("--staged", "staged"),
|
||||||
|
booleanFlag("--merge-head-first-parent", "mergeHeadFirstParent"),
|
||||||
booleanFlag("--json", "json"),
|
booleanFlag("--json", "json"),
|
||||||
booleanFlag("--github-output", "githubOutput"),
|
booleanFlag("--github-output", "githubOutput"),
|
||||||
booleanFlag("--help", "help"),
|
booleanFlag("--help", "help"),
|
||||||
@@ -538,12 +555,17 @@ if (isDirectRun()) {
|
|||||||
? args.paths
|
? args.paths
|
||||||
: args.staged
|
: args.staged
|
||||||
? listStagedChangedPaths()
|
? listStagedChangedPaths()
|
||||||
: listChangedPathsFromGit({ base: args.base, head: args.head });
|
: listChangedPathsFromGit({
|
||||||
|
base: args.base,
|
||||||
|
head: args.head,
|
||||||
|
mergeHeadFirstParent: args.mergeHeadFirstParent,
|
||||||
|
});
|
||||||
const result = detectChangedLanesForPaths({
|
const result = detectChangedLanesForPaths({
|
||||||
paths,
|
paths,
|
||||||
base: args.base,
|
base: args.base,
|
||||||
head: args.head,
|
head: args.head,
|
||||||
staged: args.staged,
|
staged: args.staged,
|
||||||
|
mergeHeadFirstParent: args.mergeHeadFirstParent,
|
||||||
});
|
});
|
||||||
if (args.githubOutput) {
|
if (args.githubOutput) {
|
||||||
writeChangedLaneGitHubOutput(result);
|
writeChangedLaneGitHubOutput(result);
|
||||||
|
|||||||
@@ -15,7 +15,12 @@ export type InstallSmokeScope = {
|
|||||||
|
|
||||||
export function detectChangedScope(changedPaths: string[]): ChangedScope;
|
export function detectChangedScope(changedPaths: string[]): ChangedScope;
|
||||||
export function detectInstallSmokeScope(changedPaths: string[]): InstallSmokeScope;
|
export function detectInstallSmokeScope(changedPaths: string[]): InstallSmokeScope;
|
||||||
export function listChangedPaths(base: string, head?: string): string[];
|
export function listChangedPaths(
|
||||||
|
base: string,
|
||||||
|
head?: string,
|
||||||
|
cwd?: string,
|
||||||
|
preferMergeHeadFirstParent?: boolean,
|
||||||
|
): string[];
|
||||||
export function writeGitHubOutput(
|
export function writeGitHubOutput(
|
||||||
scope: ChangedScope,
|
scope: ChangedScope,
|
||||||
outputPath?: string,
|
outputPath?: string,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { execFileSync } from "node:child_process";
|
import { execFileSync } from "node:child_process";
|
||||||
import { appendFileSync } from "node:fs";
|
import { appendFileSync } from "node:fs";
|
||||||
import { isDirectRunUrl } from "./lib/direct-run.mjs";
|
import { isDirectRunUrl } from "./lib/direct-run.mjs";
|
||||||
|
import { resolveMergeHeadDiffBase } from "./lib/merge-head-diff-base.mjs";
|
||||||
|
|
||||||
/** @typedef {{ runNode: boolean; runMacos: boolean; runAndroid: boolean; runWindows: boolean; runSkillsPython: boolean; runChangedSmoke: boolean; runControlUiI18n: boolean }} ChangedScope */
|
/** @typedef {{ runNode: boolean; runMacos: boolean; runAndroid: boolean; runWindows: boolean; runSkillsPython: boolean; runChangedSmoke: boolean; runControlUiI18n: boolean }} ChangedScope */
|
||||||
/** @typedef {{ runFastOnly: boolean; runPluginContracts: boolean; runCiRouting: boolean }} NodeFastScope */
|
/** @typedef {{ runFastOnly: boolean; runPluginContracts: boolean; runCiRouting: boolean }} NodeFastScope */
|
||||||
@@ -228,13 +229,26 @@ export function detectInstallSmokeScope(changedPaths) {
|
|||||||
/**
|
/**
|
||||||
* @param {string} base
|
* @param {string} base
|
||||||
* @param {string} [head]
|
* @param {string} [head]
|
||||||
|
* @param {string} [cwd]
|
||||||
* @returns {string[]}
|
* @returns {string[]}
|
||||||
*/
|
*/
|
||||||
export function listChangedPaths(base, head = "HEAD") {
|
export function listChangedPaths(
|
||||||
|
base,
|
||||||
|
head = "HEAD",
|
||||||
|
cwd = process.cwd(),
|
||||||
|
preferMergeHeadFirstParent = false,
|
||||||
|
) {
|
||||||
if (!base) {
|
if (!base) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const output = execFileSync("git", ["diff", "--name-only", base, head], {
|
const diffBase = resolveMergeHeadDiffBase({
|
||||||
|
base,
|
||||||
|
head,
|
||||||
|
cwd,
|
||||||
|
preferFirstParent: preferMergeHeadFirstParent,
|
||||||
|
});
|
||||||
|
const output = execFileSync("git", ["diff", "--name-only", diffBase, head], {
|
||||||
|
cwd,
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
encoding: "utf8",
|
encoding: "utf8",
|
||||||
});
|
});
|
||||||
@@ -293,7 +307,7 @@ function isDirectRun() {
|
|||||||
|
|
||||||
/** @param {string[]} argv */
|
/** @param {string[]} argv */
|
||||||
function parseArgs(argv) {
|
function parseArgs(argv) {
|
||||||
const args = { base: "", head: "HEAD" };
|
const args = { base: "", head: "HEAD", mergeHeadFirstParent: false };
|
||||||
for (let i = 0; i < argv.length; i += 1) {
|
for (let i = 0; i < argv.length; i += 1) {
|
||||||
if (argv[i] === "--base") {
|
if (argv[i] === "--base") {
|
||||||
args.base = argv[i + 1] ?? "";
|
args.base = argv[i + 1] ?? "";
|
||||||
@@ -303,6 +317,10 @@ function parseArgs(argv) {
|
|||||||
if (argv[i] === "--head") {
|
if (argv[i] === "--head") {
|
||||||
args.head = argv[i + 1] ?? "HEAD";
|
args.head = argv[i + 1] ?? "HEAD";
|
||||||
i += 1;
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (argv[i] === "--merge-head-first-parent") {
|
||||||
|
args.mergeHeadFirstParent = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return args;
|
return args;
|
||||||
@@ -311,7 +329,12 @@ function parseArgs(argv) {
|
|||||||
if (isDirectRun()) {
|
if (isDirectRun()) {
|
||||||
const args = parseArgs(process.argv.slice(2));
|
const args = parseArgs(process.argv.slice(2));
|
||||||
try {
|
try {
|
||||||
const changedPaths = listChangedPaths(args.base, args.head);
|
const changedPaths = listChangedPaths(
|
||||||
|
args.base,
|
||||||
|
args.head,
|
||||||
|
process.cwd(),
|
||||||
|
args.mergeHeadFirstParent,
|
||||||
|
);
|
||||||
if (changedPaths.length === 0) {
|
if (changedPaths.length === 0) {
|
||||||
writeGitHubOutput(EMPTY_SCOPE);
|
writeGitHubOutput(EMPTY_SCOPE);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|||||||
95
scripts/lib/merge-head-diff-base.mjs
Normal file
95
scripts/lib/merge-head-diff-base.mjs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
import { pathToFileURL } from "node:url";
|
||||||
|
|
||||||
|
const DEFAULT_GIT_OUTPUT_MAX_BUFFER = 16 * 1024 * 1024;
|
||||||
|
|
||||||
|
export function resolveMergeHeadDiffBase({
|
||||||
|
base,
|
||||||
|
head = "HEAD",
|
||||||
|
cwd = process.cwd(),
|
||||||
|
maxBuffer = DEFAULT_GIT_OUTPUT_MAX_BUFFER,
|
||||||
|
preferFirstParent = false,
|
||||||
|
}) {
|
||||||
|
if (!base) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (!preferFirstParent) {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parents = listCommitParents({ ref: head, cwd, maxBuffer });
|
||||||
|
if (parents.length < 2) {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstParent = resolveCommit({ ref: parents[0], cwd, maxBuffer });
|
||||||
|
const explicitBase = resolveCommit({ ref: base, cwd, maxBuffer });
|
||||||
|
if (!firstParent || firstParent === explicitBase) {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
return firstParent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function listCommitParents({ ref, cwd, maxBuffer }) {
|
||||||
|
try {
|
||||||
|
const output = execFileSync("git", ["rev-list", "--parents", "-n", "1", ref], {
|
||||||
|
cwd,
|
||||||
|
stdio: ["ignore", "pipe", "ignore"],
|
||||||
|
encoding: "utf8",
|
||||||
|
maxBuffer,
|
||||||
|
}).trim();
|
||||||
|
return output.split(/\s+/u).slice(1);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveCommit({ ref, cwd, maxBuffer }) {
|
||||||
|
try {
|
||||||
|
return execFileSync("git", ["rev-parse", "--verify", `${ref}^{commit}`], {
|
||||||
|
cwd,
|
||||||
|
stdio: ["ignore", "pipe", "ignore"],
|
||||||
|
encoding: "utf8",
|
||||||
|
maxBuffer,
|
||||||
|
}).trim();
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const args = {
|
||||||
|
base: "",
|
||||||
|
head: "HEAD",
|
||||||
|
preferFirstParent: false,
|
||||||
|
};
|
||||||
|
for (let index = 0; index < argv.length; index += 1) {
|
||||||
|
const arg = argv[index];
|
||||||
|
if (arg === "--base") {
|
||||||
|
args.base = argv[index + 1] ?? "";
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === "--head") {
|
||||||
|
args.head = argv[index + 1] ?? "HEAD";
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === "--prefer-first-parent") {
|
||||||
|
args.preferFirstParent = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||||
|
const args = parseArgs(process.argv.slice(2));
|
||||||
|
process.stdout.write(
|
||||||
|
`${resolveMergeHeadDiffBase({
|
||||||
|
base: args.base,
|
||||||
|
head: args.head,
|
||||||
|
preferFirstParent: args.preferFirstParent,
|
||||||
|
})}\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -109,9 +109,43 @@ if (( CHANGED_ONLY && PATHS_PASSED )); then
|
|||||||
exit 64
|
exit 64
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
resolve_changed_diff_ref() {
|
||||||
|
local diff_ref="${OPENCLAW_OPENGREP_BASE_REF:-origin/main...HEAD}"
|
||||||
|
local base_ref
|
||||||
|
local head_ref
|
||||||
|
local resolved_base
|
||||||
|
|
||||||
|
if [[ "$diff_ref" != *"..."* ]]; then
|
||||||
|
printf '%s\n' "$diff_ref"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ "${OPENCLAW_OPENGREP_MERGE_HEAD_FIRST_PARENT:-0}" != "1" ]]; then
|
||||||
|
printf '%s\n' "$diff_ref"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
base_ref="${diff_ref%%...*}"
|
||||||
|
head_ref="${diff_ref#*...}"
|
||||||
|
# First-parent resolution is shared with the Node CI routers so PR diff
|
||||||
|
# scope cannot drift between changed-lanes, changed-scope, and OpenGrep.
|
||||||
|
resolved_base="$(
|
||||||
|
node "$REPO_ROOT/scripts/lib/merge-head-diff-base.mjs" \
|
||||||
|
--base "$base_ref" \
|
||||||
|
--head "$head_ref" \
|
||||||
|
--prefer-first-parent 2>/dev/null || true
|
||||||
|
)"
|
||||||
|
if [[ -z "$resolved_base" || "$resolved_base" == "$base_ref" ]]; then
|
||||||
|
printf '%s\n' "$diff_ref"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s...%s\n' "$resolved_base" "$head_ref"
|
||||||
|
}
|
||||||
|
|
||||||
# Default scan paths match CI. Override by passing `-- <paths...>`.
|
# Default scan paths match CI. Override by passing `-- <paths...>`.
|
||||||
if (( PATHS_PASSED == 0 )); then
|
if (( PATHS_PASSED == 0 )); then
|
||||||
if (( CHANGED_ONLY )); then
|
if (( CHANGED_ONLY )); then
|
||||||
|
CHANGED_DIFF_REF="$(resolve_changed_diff_ref)"
|
||||||
SCAN_PATHS=()
|
SCAN_PATHS=()
|
||||||
while IFS= read -r path; do
|
while IFS= read -r path; do
|
||||||
# OpenGrep errors when an explicit changed path is a symlink; scan the
|
# OpenGrep errors when an explicit changed path is a symlink; scan the
|
||||||
@@ -125,7 +159,7 @@ if (( PATHS_PASSED == 0 )); then
|
|||||||
SCAN_PATHS+=( "$path" )
|
SCAN_PATHS+=( "$path" )
|
||||||
done < <(
|
done < <(
|
||||||
{
|
{
|
||||||
git diff --name-only --diff-filter=ACMRTUXB "${OPENCLAW_OPENGREP_BASE_REF:-origin/main...HEAD}" 2>/dev/null || true
|
git diff --name-only --diff-filter=ACMRTUXB "$CHANGED_DIFF_REF" 2>/dev/null || true
|
||||||
git diff --name-only --diff-filter=ACMRTUXB -- 2>/dev/null || true
|
git diff --name-only --diff-filter=ACMRTUXB -- 2>/dev/null || true
|
||||||
git ls-files --others --exclude-standard
|
git ls-files --others --exclude-standard
|
||||||
} | awk '/^(src|extensions|apps|packages|scripts)\// { print }' | sort -u
|
} | awk '/^(src|extensions|apps|packages|scripts)\// { print }' | sort -u
|
||||||
@@ -135,7 +169,7 @@ if (( PATHS_PASSED == 0 )); then
|
|||||||
RULEPACK_CHANGED_PATHS+=( "$path" )
|
RULEPACK_CHANGED_PATHS+=( "$path" )
|
||||||
done < <(
|
done < <(
|
||||||
{
|
{
|
||||||
git diff --name-only --diff-filter=ACMRTUXB "${OPENCLAW_OPENGREP_BASE_REF:-origin/main...HEAD}" 2>/dev/null || true
|
git diff --name-only --diff-filter=ACMRTUXB "$CHANGED_DIFF_REF" 2>/dev/null || true
|
||||||
git diff --name-only --diff-filter=ACMRTUXB -- 2>/dev/null || true
|
git diff --name-only --diff-filter=ACMRTUXB -- 2>/dev/null || true
|
||||||
git ls-files --others --exclude-standard
|
git ls-files --others --exclude-standard
|
||||||
} | awk '/^(security\/opengrep\/|scripts\/run-opengrep\.sh$|\.semgrepignore$|\.github\/workflows\/opengrep-)/ { print }' | sort -u
|
} | awk '/^(security\/opengrep\/|scripts\/run-opengrep\.sh$|\.semgrepignore$|\.github\/workflows\/opengrep-)/ { print }' | sort -u
|
||||||
|
|||||||
@@ -256,5 +256,5 @@ export function promptMigrationSkillSelectionValues(
|
|||||||
return prompt.prompt();
|
return prompt.prompt();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Back-compat alias for plugin selection prompts that share the same picker. */
|
/** Compatibility alias for plugin selection prompts that share the same picker. */
|
||||||
export const promptMigrationSelectionValues = promptMigrationSkillSelectionValues;
|
export const promptMigrationSelectionValues = promptMigrationSkillSelectionValues;
|
||||||
|
|||||||
@@ -515,8 +515,10 @@ function collectDeprecatedTestBarrelImports(): string[] {
|
|||||||
function collectDeprecatedPackageTestingBridgeDrift(): string[] {
|
function collectDeprecatedPackageTestingBridgeDrift(): string[] {
|
||||||
const source = fs
|
const source = fs
|
||||||
.readFileSync(resolve(REPO_ROOT, "packages/plugin-sdk/src/testing.ts"), "utf8")
|
.readFileSync(resolve(REPO_ROOT, "packages/plugin-sdk/src/testing.ts"), "utf8")
|
||||||
.trim();
|
.split("\n")
|
||||||
return source === 'export * from "../../../src/plugin-sdk/testing.js";'
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => line && !line.startsWith("//"));
|
||||||
|
return source.length === 1 && source[0] === 'export * from "../../../src/plugin-sdk/testing.js";'
|
||||||
? []
|
? []
|
||||||
: ["packages/plugin-sdk/src/testing.ts"];
|
: ["packages/plugin-sdk/src/testing.ts"];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { resolveGlobalSingleton } from "../shared/global-singleton.js";
|
|||||||
import { withPluginHostCleanupTimeout } from "./host-hook-cleanup-timeout.js";
|
import { withPluginHostCleanupTimeout } from "./host-hook-cleanup-timeout.js";
|
||||||
import {
|
import {
|
||||||
isPluginJsonValue,
|
isPluginJsonValue,
|
||||||
|
type PluginAgentEventSubscriptionRegistration,
|
||||||
type PluginHostCleanupReason,
|
type PluginHostCleanupReason,
|
||||||
type PluginJsonValue,
|
type PluginJsonValue,
|
||||||
type PluginRunContextGetParams,
|
type PluginRunContextGetParams,
|
||||||
@@ -17,6 +18,9 @@ import type { PluginRegistry } from "./registry-types.js";
|
|||||||
|
|
||||||
type PluginRunContextNamespaces = Map<string, PluginJsonValue>;
|
type PluginRunContextNamespaces = Map<string, PluginJsonValue>;
|
||||||
type PluginRunContextByPlugin = Map<string, PluginRunContextNamespaces>;
|
type PluginRunContextByPlugin = Map<string, PluginRunContextNamespaces>;
|
||||||
|
type PluginAgentEventSubscriptionContext = Parameters<
|
||||||
|
PluginAgentEventSubscriptionRegistration["handle"]
|
||||||
|
>[1];
|
||||||
|
|
||||||
type SchedulerJobRecord = {
|
type SchedulerJobRecord = {
|
||||||
pluginId: string;
|
pluginId: string;
|
||||||
@@ -305,10 +309,12 @@ export function dispatchPluginAgentEventSubscriptions(params: {
|
|||||||
const pluginId = registration.pluginId;
|
const pluginId = registration.pluginId;
|
||||||
const runId = params.event.runId;
|
const runId = params.event.runId;
|
||||||
let handlerActive = true;
|
let handlerActive = true;
|
||||||
const ctx = {
|
const ctx: PluginAgentEventSubscriptionContext = {
|
||||||
// oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- Run-context JSON reads are caller-typed by namespace.
|
getRunContext: ((namespace: string) =>
|
||||||
getRunContext: <T extends PluginJsonValue = PluginJsonValue>(namespace: string) =>
|
getPluginRunContext({
|
||||||
getPluginRunContext({ pluginId, get: { runId, namespace } }) as T | undefined,
|
pluginId,
|
||||||
|
get: { runId, namespace },
|
||||||
|
})) as PluginAgentEventSubscriptionContext["getRunContext"],
|
||||||
setRunContext: (namespace: string, value: PluginJsonValue) => {
|
setRunContext: (namespace: string, value: PluginJsonValue) => {
|
||||||
setPluginRunContext({
|
setPluginRunContext({
|
||||||
pluginId,
|
pluginId,
|
||||||
|
|||||||
@@ -25,7 +25,12 @@ const { detectChangedScope, detectInstallSmokeScope, detectNodeFastScope, listCh
|
|||||||
runPluginContracts: boolean;
|
runPluginContracts: boolean;
|
||||||
runCiRouting: boolean;
|
runCiRouting: boolean;
|
||||||
};
|
};
|
||||||
listChangedPaths: (base: string, head?: string) => string[];
|
listChangedPaths: (
|
||||||
|
base: string,
|
||||||
|
head?: string,
|
||||||
|
cwd?: string,
|
||||||
|
preferMergeHeadFirstParent?: boolean,
|
||||||
|
) => string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const markerPaths: string[] = [];
|
const markerPaths: string[] = [];
|
||||||
@@ -56,6 +61,42 @@ function parseGitHubOutput(output: string): Record<string, string> {
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function git(repoDir: string, args: string[]): string {
|
||||||
|
return execFileSync("git", args, { cwd: repoDir, encoding: "utf8" }).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeRepoFile(repoDir: string, filePath: string, contents: string): void {
|
||||||
|
const absolutePath = path.join(repoDir, filePath);
|
||||||
|
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
||||||
|
fs.writeFileSync(absolutePath, contents, "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSyntheticMergeRepo(prefix: string): { repoDir: string; staleBase: string } {
|
||||||
|
const repoDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||||
|
tempDirs.push(repoDir);
|
||||||
|
|
||||||
|
git(repoDir, ["init", "-b", "main"]);
|
||||||
|
git(repoDir, ["config", "user.email", "ci@example.invalid"]);
|
||||||
|
git(repoDir, ["config", "user.name", "CI"]);
|
||||||
|
writeRepoFile(repoDir, "README.md", "base\n");
|
||||||
|
git(repoDir, ["add", "."]);
|
||||||
|
git(repoDir, ["commit", "-m", "base"]);
|
||||||
|
const staleBase = git(repoDir, ["rev-parse", "HEAD"]);
|
||||||
|
|
||||||
|
git(repoDir, ["switch", "-c", "feature"]);
|
||||||
|
writeRepoFile(repoDir, "src/pr.ts", "export const pr = true;\n");
|
||||||
|
git(repoDir, ["add", "."]);
|
||||||
|
git(repoDir, ["commit", "-m", "feature"]);
|
||||||
|
|
||||||
|
git(repoDir, ["switch", "main"]);
|
||||||
|
writeRepoFile(repoDir, "src/main-only.ts", "export const mainOnly = true;\n");
|
||||||
|
git(repoDir, ["add", "."]);
|
||||||
|
git(repoDir, ["commit", "-m", "main only"]);
|
||||||
|
git(repoDir, ["merge", "--no-ff", "feature", "-m", "synthetic merge"]);
|
||||||
|
|
||||||
|
return { repoDir, staleBase };
|
||||||
|
}
|
||||||
|
|
||||||
describe("detectChangedScope", () => {
|
describe("detectChangedScope", () => {
|
||||||
it("fails safe when no paths are provided", () => {
|
it("fails safe when no paths are provided", () => {
|
||||||
expect(detectChangedScope([])).toEqual({
|
expect(detectChangedScope([])).toEqual({
|
||||||
@@ -652,6 +693,22 @@ describe("detectChangedScope", () => {
|
|||||||
expect(fs.existsSync(markerPath)).toBe(false);
|
expect(fs.existsSync(markerPath)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses the merge commit first parent instead of a stale PR payload base", () => {
|
||||||
|
const { repoDir, staleBase } = createSyntheticMergeRepo("openclaw-ci-scope-merge-");
|
||||||
|
|
||||||
|
expect(
|
||||||
|
execFileSync("git", ["diff", "--name-only", staleBase, "HEAD"], {
|
||||||
|
cwd: repoDir,
|
||||||
|
encoding: "utf8",
|
||||||
|
})
|
||||||
|
.trim()
|
||||||
|
.split("\n")
|
||||||
|
.toSorted(),
|
||||||
|
).toEqual(["src/main-only.ts", "src/pr.ts"]);
|
||||||
|
|
||||||
|
expect(listChangedPaths(staleBase, "HEAD", repoDir, true)).toEqual(["src/pr.ts"]);
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps direct CLI preflight empty diffs as no-op scope", () => {
|
it("keeps direct CLI preflight empty diffs as no-op scope", () => {
|
||||||
const repoDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-ci-scope-empty-"));
|
const repoDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-ci-scope-empty-"));
|
||||||
tempDirs.push(repoDir);
|
tempDirs.push(repoDir);
|
||||||
|
|||||||
@@ -71,6 +71,71 @@ function parseChangedLaneOutput(output: string): {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function writeRepoFile(repoDir: string, filePath: string, contents: string): void {
|
||||||
|
const absolutePath = path.join(repoDir, filePath);
|
||||||
|
mkdirSync(path.dirname(absolutePath), { recursive: true });
|
||||||
|
writeFileSync(absolutePath, contents, "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSyntheticMergeRepo(prefix: string): { dir: string; staleBase: string } {
|
||||||
|
const dir = makeTempRepoRoot(tempDirs, prefix);
|
||||||
|
git(dir, ["init", "-q", "--initial-branch=main"]);
|
||||||
|
writeRepoFile(dir, "README.md", "base\n");
|
||||||
|
git(dir, ["add", "."]);
|
||||||
|
git(dir, [
|
||||||
|
"-c",
|
||||||
|
"user.email=test@example.com",
|
||||||
|
"-c",
|
||||||
|
"user.name=Test User",
|
||||||
|
"commit",
|
||||||
|
"-q",
|
||||||
|
"-m",
|
||||||
|
"base",
|
||||||
|
]);
|
||||||
|
const staleBase = git(dir, ["rev-parse", "HEAD"]);
|
||||||
|
|
||||||
|
git(dir, ["switch", "-q", "-c", "feature"]);
|
||||||
|
writeRepoFile(dir, "src/pr.ts", "export const pr = true;\n");
|
||||||
|
git(dir, ["add", "."]);
|
||||||
|
git(dir, [
|
||||||
|
"-c",
|
||||||
|
"user.email=test@example.com",
|
||||||
|
"-c",
|
||||||
|
"user.name=Test User",
|
||||||
|
"commit",
|
||||||
|
"-q",
|
||||||
|
"-m",
|
||||||
|
"feature",
|
||||||
|
]);
|
||||||
|
|
||||||
|
git(dir, ["switch", "-q", "main"]);
|
||||||
|
writeRepoFile(dir, "src/main-only.ts", "export const mainOnly = true;\n");
|
||||||
|
git(dir, ["add", "."]);
|
||||||
|
git(dir, [
|
||||||
|
"-c",
|
||||||
|
"user.email=test@example.com",
|
||||||
|
"-c",
|
||||||
|
"user.name=Test User",
|
||||||
|
"commit",
|
||||||
|
"-q",
|
||||||
|
"-m",
|
||||||
|
"main only",
|
||||||
|
]);
|
||||||
|
git(dir, [
|
||||||
|
"-c",
|
||||||
|
"user.email=test@example.com",
|
||||||
|
"-c",
|
||||||
|
"user.name=Test User",
|
||||||
|
"merge",
|
||||||
|
"--no-ff",
|
||||||
|
"feature",
|
||||||
|
"-m",
|
||||||
|
"synthetic merge",
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { dir, staleBase };
|
||||||
|
}
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cleanupCorepackPnpmShimDir();
|
cleanupCorepackPnpmShimDir();
|
||||||
cleanupTempDirs(tempDirs);
|
cleanupTempDirs(tempDirs);
|
||||||
@@ -251,6 +316,23 @@ describe("scripts/changed-lanes", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses the merge commit first parent instead of a stale PR payload base", () => {
|
||||||
|
const { dir, staleBase } = createSyntheticMergeRepo("openclaw-changed-lanes-merge-");
|
||||||
|
|
||||||
|
expect(listChangedPathsFromGit({ base: staleBase, cwd: dir, includeWorktree: false })).toEqual([
|
||||||
|
"src/main-only.ts",
|
||||||
|
"src/pr.ts",
|
||||||
|
]);
|
||||||
|
expect(
|
||||||
|
listChangedPathsFromGit({
|
||||||
|
base: staleBase,
|
||||||
|
cwd: dir,
|
||||||
|
includeWorktree: false,
|
||||||
|
mergeHeadFirstParent: true,
|
||||||
|
}),
|
||||||
|
).toEqual(["src/pr.ts"]);
|
||||||
|
});
|
||||||
|
|
||||||
it("ignores local Crabbox metadata in the default local diff", () => {
|
it("ignores local Crabbox metadata in the default local diff", () => {
|
||||||
const dir = makeTempRepoRoot(tempDirs, "openclaw-changed-lanes-crabbox-");
|
const dir = makeTempRepoRoot(tempDirs, "openclaw-changed-lanes-crabbox-");
|
||||||
git(dir, ["init", "-q", "--initial-branch=main"]);
|
git(dir, ["init", "-q", "--initial-branch=main"]);
|
||||||
|
|||||||
@@ -59,8 +59,9 @@ describe("ci workflow guards", () => {
|
|||||||
expect(checkoutStep.run, jobName).toContain("timed out on attempt $attempt; retrying");
|
expect(checkoutStep.run, jobName).toContain("timed out on attempt $attempt; retrying");
|
||||||
expect(checkoutStep.run, jobName).not.toContain("if timeout --signal=TERM");
|
expect(checkoutStep.run, jobName).not.toContain("if timeout --signal=TERM");
|
||||||
expect(checkoutStep.run, jobName).toContain("-c protocol.version=2");
|
expect(checkoutStep.run, jobName).toContain("-c protocol.version=2");
|
||||||
|
const expectedDepth = jobName === "preflight" ? 2 : 1;
|
||||||
expect(checkoutStep.run, jobName).toContain(
|
expect(checkoutStep.run, jobName).toContain(
|
||||||
"fetch --no-tags --prune --no-recurse-submodules --depth=1 origin",
|
`fetch --no-tags --prune --no-recurse-submodules --depth=${expectedDepth} origin`,
|
||||||
);
|
);
|
||||||
if (jobName !== "skills-python") {
|
if (jobName !== "skills-python") {
|
||||||
expect(checkoutStep.run, jobName).toContain('if [ "$fetch_status" = "124" ]');
|
expect(checkoutStep.run, jobName).toContain('if [ "$fetch_status" = "124" ]');
|
||||||
|
|||||||
@@ -214,7 +214,6 @@ describe("production lint suppressions", () => {
|
|||||||
"src/plugin-sdk/test-helpers/public-surface-loader.ts|typescript/no-unnecessary-type-parameters|1",
|
"src/plugin-sdk/test-helpers/public-surface-loader.ts|typescript/no-unnecessary-type-parameters|1",
|
||||||
"src/plugin-sdk/test-helpers/subagent-hooks.ts|typescript/no-unnecessary-type-parameters|1",
|
"src/plugin-sdk/test-helpers/subagent-hooks.ts|typescript/no-unnecessary-type-parameters|1",
|
||||||
"src/plugins/hooks.ts|typescript/no-unnecessary-type-parameters|1",
|
"src/plugins/hooks.ts|typescript/no-unnecessary-type-parameters|1",
|
||||||
"src/plugins/host-hook-runtime.ts|typescript/no-unnecessary-type-parameters|1",
|
|
||||||
"src/plugins/host-hook-state.ts|typescript/no-unnecessary-type-parameters|1",
|
"src/plugins/host-hook-state.ts|typescript/no-unnecessary-type-parameters|1",
|
||||||
"src/plugins/host-hooks.ts|typescript/no-unnecessary-type-parameters|1",
|
"src/plugins/host-hooks.ts|typescript/no-unnecessary-type-parameters|1",
|
||||||
"src/plugins/lazy-service-module.ts|typescript/no-unnecessary-type-parameters|1",
|
"src/plugins/lazy-service-module.ts|typescript/no-unnecessary-type-parameters|1",
|
||||||
|
|||||||
@@ -15,6 +15,17 @@ function writeFile(filePath: string, content: string): void {
|
|||||||
fs.writeFileSync(filePath, content);
|
fs.writeFileSync(filePath, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function copyRunOpengrepFiles(repo: string): void {
|
||||||
|
const scriptSource = path.resolve("scripts/run-opengrep.sh");
|
||||||
|
const helperSource = path.resolve("scripts/lib/merge-head-diff-base.mjs");
|
||||||
|
writeFile(path.join(repo, "scripts/run-opengrep.sh"), fs.readFileSync(scriptSource, "utf8"));
|
||||||
|
writeFile(
|
||||||
|
path.join(repo, "scripts/lib/merge-head-diff-base.mjs"),
|
||||||
|
fs.readFileSync(helperSource, "utf8"),
|
||||||
|
);
|
||||||
|
fs.chmodSync(path.join(repo, "scripts/run-opengrep.sh"), 0o755);
|
||||||
|
}
|
||||||
|
|
||||||
describe("run-opengrep.sh", () => {
|
describe("run-opengrep.sh", () => {
|
||||||
it("validates the rulepack when only OpenGrep rulepack files changed", () => {
|
it("validates the rulepack when only OpenGrep rulepack files changed", () => {
|
||||||
const repo = createTempDir("openclaw-run-opengrep-");
|
const repo = createTempDir("openclaw-run-opengrep-");
|
||||||
@@ -22,9 +33,7 @@ describe("run-opengrep.sh", () => {
|
|||||||
git(repo, "config", "user.email", "test@example.com");
|
git(repo, "config", "user.email", "test@example.com");
|
||||||
git(repo, "config", "user.name", "Test User");
|
git(repo, "config", "user.name", "Test User");
|
||||||
|
|
||||||
const scriptSource = path.resolve("scripts/run-opengrep.sh");
|
copyRunOpengrepFiles(repo);
|
||||||
writeFile(path.join(repo, "scripts/run-opengrep.sh"), fs.readFileSync(scriptSource, "utf8"));
|
|
||||||
fs.chmodSync(path.join(repo, "scripts/run-opengrep.sh"), 0o755);
|
|
||||||
writeFile(path.join(repo, "security/opengrep/precise.yml"), "rules: []\n");
|
writeFile(path.join(repo, "security/opengrep/precise.yml"), "rules: []\n");
|
||||||
git(repo, "add", ".");
|
git(repo, "add", ".");
|
||||||
git(repo, "commit", "-qm", "initial");
|
git(repo, "commit", "-qm", "initial");
|
||||||
@@ -57,4 +66,58 @@ describe("run-opengrep.sh", () => {
|
|||||||
const args = fs.readFileSync(path.join(repo, "opengrep-args.txt"), "utf8");
|
const args = fs.readFileSync(path.join(repo, "opengrep-args.txt"), "utf8");
|
||||||
expect(args).toContain("security/opengrep/precise.yml");
|
expect(args).toContain("security/opengrep/precise.yml");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("scans PR files instead of main-only files when the payload base is stale", () => {
|
||||||
|
const repo = createTempDir("openclaw-run-opengrep-merge-");
|
||||||
|
git(repo, "init", "-q", "--initial-branch=main");
|
||||||
|
git(repo, "config", "user.email", "test@example.com");
|
||||||
|
git(repo, "config", "user.name", "Test User");
|
||||||
|
|
||||||
|
copyRunOpengrepFiles(repo);
|
||||||
|
writeFile(path.join(repo, "security/opengrep/precise.yml"), "rules: []\n");
|
||||||
|
writeFile(path.join(repo, "README.md"), "base\n");
|
||||||
|
git(repo, "add", ".");
|
||||||
|
git(repo, "commit", "-qm", "base");
|
||||||
|
const staleBase = git(repo, "rev-parse", "HEAD");
|
||||||
|
|
||||||
|
git(repo, "switch", "-q", "-c", "feature");
|
||||||
|
writeFile(path.join(repo, "src/pr.ts"), "export const pr = true;\n");
|
||||||
|
git(repo, "add", ".");
|
||||||
|
git(repo, "commit", "-qm", "feature");
|
||||||
|
|
||||||
|
git(repo, "switch", "-q", "main");
|
||||||
|
writeFile(path.join(repo, "src/main-only.ts"), "export const mainOnly = true;\n");
|
||||||
|
git(repo, "add", ".");
|
||||||
|
git(repo, "commit", "-qm", "main only");
|
||||||
|
git(repo, "merge", "--no-ff", "feature", "-m", "synthetic merge");
|
||||||
|
|
||||||
|
const argsPath = path.join(repo, "opengrep-args.txt");
|
||||||
|
const binDir = path.join(repo, "bin");
|
||||||
|
fs.mkdirSync(binDir);
|
||||||
|
writeFile(
|
||||||
|
path.join(binDir, "opengrep"),
|
||||||
|
[
|
||||||
|
"#!/usr/bin/env bash",
|
||||||
|
`printf '%s\\n' "$@" > ${JSON.stringify(argsPath)}`,
|
||||||
|
"exit 0",
|
||||||
|
"",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
fs.chmodSync(path.join(binDir, "opengrep"), 0o755);
|
||||||
|
|
||||||
|
execFileSync("bash", ["scripts/run-opengrep.sh", "--changed"], {
|
||||||
|
cwd: repo,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ""}`,
|
||||||
|
OPENCLAW_OPENGREP_BASE_REF: `${staleBase}...HEAD`,
|
||||||
|
OPENCLAW_OPENGREP_MERGE_HEAD_FIRST_PARENT: "1",
|
||||||
|
},
|
||||||
|
encoding: "utf8",
|
||||||
|
});
|
||||||
|
|
||||||
|
const args = fs.readFileSync(argsPath, "utf8");
|
||||||
|
expect(args).toContain("src/pr.ts");
|
||||||
|
expect(args).not.toContain("src/main-only.ts");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user