Skip to content

Commit 8346f9f

Browse files
authored
Merge 40235e8 into 9b7ad24
2 parents 9b7ad24 + 40235e8 commit 8346f9f

15 files changed

Lines changed: 419 additions & 29 deletions

.github/workflows/ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ jobs:
9292
for attempt in 1 2 3; do
9393
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
9494
-c protocol.version=2 \
95-
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
95+
fetch --no-tags --prune --no-recurse-submodules --depth=2 origin \
9696
"+${ref}:refs/remotes/origin/checkout" && return 0
9797
fetch_status="$?"
9898
if [ "$fetch_status" != "124" ] && [ "$fetch_status" != "137" ]; then
@@ -146,12 +146,12 @@ jobs:
146146
147147
if [ "${{ github.event_name }}" = "push" ]; then
148148
BASE="${{ github.event.before }}"
149+
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD
149150
else
150151
BASE="${{ github.event.pull_request.base.sha }}"
152+
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD --merge-head-first-parent
151153
fi
152154
153-
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD
154-
155155
- name: Build CI manifest
156156
id: manifest
157157
env:

.github/workflows/opengrep-precise.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ jobs:
4444
uses: actions/checkout@v6
4545
with:
4646
ref: ${{ github.sha }}
47-
fetch-depth: 1
47+
fetch-depth: 2
4848
fetch-tags: false
4949
persist-credentials: false
5050
submodules: false
@@ -74,6 +74,7 @@ jobs:
7474
- name: Run opengrep on PR diff
7575
env:
7676
OPENCLAW_OPENGREP_BASE_REF: ${{ github.event.pull_request.base.sha }}...HEAD
77+
OPENCLAW_OPENGREP_MERGE_HEAD_FIRST_PARENT: "1"
7778
# Findings from precise rules block this workflow. Pull requests scan
7879
# changed first-party source paths only so findings stay attributable to
7980
# the PR diff. Test/fixture/QA path exclusions live in `.semgrepignore`

scripts/changed-lanes.mjs

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { execFileSync } from "node:child_process";
22
import { appendFileSync, existsSync, readFileSync } from "node:fs";
33
import { booleanFlag, parseFlagArgs, stringFlag } from "./lib/arg-utils.mjs";
44
import { isDirectRunUrl } from "./lib/direct-run.mjs";
5+
import { resolveMergeHeadDiffBase } from "./lib/merge-head-diff-base.mjs";
56

67
const GIT_OUTPUT_MAX_BUFFER = 64 * 1024 * 1024;
78
const IMPLAUSIBLE_NO_MERGE_BASE_DIFF_PATHS = 200;
@@ -213,13 +214,21 @@ export function detectChangedLanes(changedPaths, options = {}) {
213214
}
214215

215216
/**
216-
* @param {{ paths: string[]; base: string; head?: string; staged?: boolean }} params
217+
* @param {{ paths: string[]; base: string; head?: string; staged?: boolean; mergeHeadFirstParent?: boolean }} params
217218
* @returns {ChangedLaneResult}
218219
*/
219220
export function detectChangedLanesForPaths(params) {
221+
const base = params.staged
222+
? params.base
223+
: resolveMergeHeadDiffBase({
224+
base: params.base,
225+
head: params.head ?? "HEAD",
226+
maxBuffer: GIT_OUTPUT_MAX_BUFFER,
227+
preferFirstParent: params.mergeHeadFirstParent === true,
228+
});
220229
const packageJsonChangeKind = params.paths.includes("package.json")
221230
? classifyPackageJsonChangeFromGit({
222-
base: params.base,
231+
base,
223232
head: params.head,
224233
staged: params.staged,
225234
})
@@ -228,13 +237,19 @@ export function detectChangedLanesForPaths(params) {
228237
}
229238

230239
/**
231-
* @param {{ base: string; head?: string; includeWorktree?: boolean; cwd?: string }} params
240+
* @param {{ base: string; head?: string; includeWorktree?: boolean; cwd?: string; mergeHeadFirstParent?: boolean }} params
232241
* @returns {string[]}
233242
*/
234243
export function listChangedPathsFromGit(params) {
235-
const base = params.base;
236244
const head = params.head ?? "HEAD";
237245
const cwd = params.cwd ?? process.cwd();
246+
const base = resolveMergeHeadDiffBase({
247+
base: params.base,
248+
head,
249+
cwd,
250+
maxBuffer: GIT_OUTPUT_MAX_BUFFER,
251+
preferFirstParent: params.mergeHeadFirstParent === true,
252+
});
238253
if (!base) {
239254
return [];
240255
}
@@ -453,6 +468,7 @@ function parseArgs(argv) {
453468
base: "origin/main",
454469
head: "HEAD",
455470
staged: false,
471+
mergeHeadFirstParent: false,
456472
json: false,
457473
githubOutput: false,
458474
help: false,
@@ -465,6 +481,7 @@ function parseArgs(argv) {
465481
stringFlag("--base", "base"),
466482
stringFlag("--head", "head"),
467483
booleanFlag("--staged", "staged"),
484+
booleanFlag("--merge-head-first-parent", "mergeHeadFirstParent"),
468485
booleanFlag("--json", "json"),
469486
booleanFlag("--github-output", "githubOutput"),
470487
booleanFlag("--help", "help"),
@@ -538,12 +555,17 @@ if (isDirectRun()) {
538555
? args.paths
539556
: args.staged
540557
? listStagedChangedPaths()
541-
: listChangedPathsFromGit({ base: args.base, head: args.head });
558+
: listChangedPathsFromGit({
559+
base: args.base,
560+
head: args.head,
561+
mergeHeadFirstParent: args.mergeHeadFirstParent,
562+
});
542563
const result = detectChangedLanesForPaths({
543564
paths,
544565
base: args.base,
545566
head: args.head,
546567
staged: args.staged,
568+
mergeHeadFirstParent: args.mergeHeadFirstParent,
547569
});
548570
if (args.githubOutput) {
549571
writeChangedLaneGitHubOutput(result);

scripts/ci-changed-scope.d.mts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@ export type InstallSmokeScope = {
1515

1616
export function detectChangedScope(changedPaths: string[]): ChangedScope;
1717
export function detectInstallSmokeScope(changedPaths: string[]): InstallSmokeScope;
18-
export function listChangedPaths(base: string, head?: string): string[];
18+
export function listChangedPaths(
19+
base: string,
20+
head?: string,
21+
cwd?: string,
22+
preferMergeHeadFirstParent?: boolean,
23+
): string[];
1924
export function writeGitHubOutput(
2025
scope: ChangedScope,
2126
outputPath?: string,

scripts/ci-changed-scope.mjs

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { execFileSync } from "node:child_process";
22
import { appendFileSync } from "node:fs";
33
import { isDirectRunUrl } from "./lib/direct-run.mjs";
4+
import { resolveMergeHeadDiffBase } from "./lib/merge-head-diff-base.mjs";
45

56
/** @typedef {{ runNode: boolean; runMacos: boolean; runAndroid: boolean; runWindows: boolean; runSkillsPython: boolean; runChangedSmoke: boolean; runControlUiI18n: boolean }} ChangedScope */
67
/** @typedef {{ runFastOnly: boolean; runPluginContracts: boolean; runCiRouting: boolean }} NodeFastScope */
@@ -228,13 +229,26 @@ export function detectInstallSmokeScope(changedPaths) {
228229
/**
229230
* @param {string} base
230231
* @param {string} [head]
232+
* @param {string} [cwd]
231233
* @returns {string[]}
232234
*/
233-
export function listChangedPaths(base, head = "HEAD") {
235+
export function listChangedPaths(
236+
base,
237+
head = "HEAD",
238+
cwd = process.cwd(),
239+
preferMergeHeadFirstParent = false,
240+
) {
234241
if (!base) {
235242
return [];
236243
}
237-
const output = execFileSync("git", ["diff", "--name-only", base, head], {
244+
const diffBase = resolveMergeHeadDiffBase({
245+
base,
246+
head,
247+
cwd,
248+
preferFirstParent: preferMergeHeadFirstParent,
249+
});
250+
const output = execFileSync("git", ["diff", "--name-only", diffBase, head], {
251+
cwd,
238252
stdio: ["ignore", "pipe", "pipe"],
239253
encoding: "utf8",
240254
});
@@ -293,7 +307,7 @@ function isDirectRun() {
293307

294308
/** @param {string[]} argv */
295309
function parseArgs(argv) {
296-
const args = { base: "", head: "HEAD" };
310+
const args = { base: "", head: "HEAD", mergeHeadFirstParent: false };
297311
for (let i = 0; i < argv.length; i += 1) {
298312
if (argv[i] === "--base") {
299313
args.base = argv[i + 1] ?? "";
@@ -303,6 +317,10 @@ function parseArgs(argv) {
303317
if (argv[i] === "--head") {
304318
args.head = argv[i + 1] ?? "HEAD";
305319
i += 1;
320+
continue;
321+
}
322+
if (argv[i] === "--merge-head-first-parent") {
323+
args.mergeHeadFirstParent = true;
306324
}
307325
}
308326
return args;
@@ -311,7 +329,12 @@ function parseArgs(argv) {
311329
if (isDirectRun()) {
312330
const args = parseArgs(process.argv.slice(2));
313331
try {
314-
const changedPaths = listChangedPaths(args.base, args.head);
332+
const changedPaths = listChangedPaths(
333+
args.base,
334+
args.head,
335+
process.cwd(),
336+
args.mergeHeadFirstParent,
337+
);
315338
if (changedPaths.length === 0) {
316339
writeGitHubOutput(EMPTY_SCOPE);
317340
process.exit(0);
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { execFileSync } from "node:child_process";
2+
import { pathToFileURL } from "node:url";
3+
4+
const DEFAULT_GIT_OUTPUT_MAX_BUFFER = 16 * 1024 * 1024;
5+
6+
export function resolveMergeHeadDiffBase({
7+
base,
8+
head = "HEAD",
9+
cwd = process.cwd(),
10+
maxBuffer = DEFAULT_GIT_OUTPUT_MAX_BUFFER,
11+
preferFirstParent = false,
12+
}) {
13+
if (!base) {
14+
return "";
15+
}
16+
if (!preferFirstParent) {
17+
return base;
18+
}
19+
20+
const parents = listCommitParents({ ref: head, cwd, maxBuffer });
21+
if (parents.length < 2) {
22+
return base;
23+
}
24+
25+
const firstParent = resolveCommit({ ref: parents[0], cwd, maxBuffer });
26+
const explicitBase = resolveCommit({ ref: base, cwd, maxBuffer });
27+
if (!firstParent || firstParent === explicitBase) {
28+
return base;
29+
}
30+
31+
return firstParent;
32+
}
33+
34+
function listCommitParents({ ref, cwd, maxBuffer }) {
35+
try {
36+
const output = execFileSync("git", ["rev-list", "--parents", "-n", "1", ref], {
37+
cwd,
38+
stdio: ["ignore", "pipe", "ignore"],
39+
encoding: "utf8",
40+
maxBuffer,
41+
}).trim();
42+
return output.split(/\s+/u).slice(1);
43+
} catch {
44+
return [];
45+
}
46+
}
47+
48+
function resolveCommit({ ref, cwd, maxBuffer }) {
49+
try {
50+
return execFileSync("git", ["rev-parse", "--verify", `${ref}^{commit}`], {
51+
cwd,
52+
stdio: ["ignore", "pipe", "ignore"],
53+
encoding: "utf8",
54+
maxBuffer,
55+
}).trim();
56+
} catch {
57+
return "";
58+
}
59+
}
60+
61+
function parseArgs(argv) {
62+
const args = {
63+
base: "",
64+
head: "HEAD",
65+
preferFirstParent: false,
66+
};
67+
for (let index = 0; index < argv.length; index += 1) {
68+
const arg = argv[index];
69+
if (arg === "--base") {
70+
args.base = argv[index + 1] ?? "";
71+
index += 1;
72+
continue;
73+
}
74+
if (arg === "--head") {
75+
args.head = argv[index + 1] ?? "HEAD";
76+
index += 1;
77+
continue;
78+
}
79+
if (arg === "--prefer-first-parent") {
80+
args.preferFirstParent = true;
81+
}
82+
}
83+
return args;
84+
}
85+
86+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
87+
const args = parseArgs(process.argv.slice(2));
88+
process.stdout.write(
89+
`${resolveMergeHeadDiffBase({
90+
base: args.base,
91+
head: args.head,
92+
preferFirstParent: args.preferFirstParent,
93+
})}\n`,
94+
);
95+
}

scripts/run-opengrep.sh

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,43 @@ if (( CHANGED_ONLY && PATHS_PASSED )); then
109109
exit 64
110110
fi
111111

112+
resolve_changed_diff_ref() {
113+
local diff_ref="${OPENCLAW_OPENGREP_BASE_REF:-origin/main...HEAD}"
114+
local base_ref
115+
local head_ref
116+
local resolved_base
117+
118+
if [[ "$diff_ref" != *"..."* ]]; then
119+
printf '%s\n' "$diff_ref"
120+
return 0
121+
fi
122+
if [[ "${OPENCLAW_OPENGREP_MERGE_HEAD_FIRST_PARENT:-0}" != "1" ]]; then
123+
printf '%s\n' "$diff_ref"
124+
return 0
125+
fi
126+
127+
base_ref="${diff_ref%%...*}"
128+
head_ref="${diff_ref#*...}"
129+
# First-parent resolution is shared with the Node CI routers so PR diff
130+
# scope cannot drift between changed-lanes, changed-scope, and OpenGrep.
131+
resolved_base="$(
132+
node "$REPO_ROOT/scripts/lib/merge-head-diff-base.mjs" \
133+
--base "$base_ref" \
134+
--head "$head_ref" \
135+
--prefer-first-parent 2>/dev/null || true
136+
)"
137+
if [[ -z "$resolved_base" || "$resolved_base" == "$base_ref" ]]; then
138+
printf '%s\n' "$diff_ref"
139+
return 0
140+
fi
141+
142+
printf '%s...%s\n' "$resolved_base" "$head_ref"
143+
}
144+
112145
# Default scan paths match CI. Override by passing `-- <paths...>`.
113146
if (( PATHS_PASSED == 0 )); then
114147
if (( CHANGED_ONLY )); then
148+
CHANGED_DIFF_REF="$(resolve_changed_diff_ref)"
115149
SCAN_PATHS=()
116150
while IFS= read -r path; do
117151
# OpenGrep errors when an explicit changed path is a symlink; scan the
@@ -125,7 +159,7 @@ if (( PATHS_PASSED == 0 )); then
125159
SCAN_PATHS+=( "$path" )
126160
done < <(
127161
{
128-
git diff --name-only --diff-filter=ACMRTUXB "${OPENCLAW_OPENGREP_BASE_REF:-origin/main...HEAD}" 2>/dev/null || true
162+
git diff --name-only --diff-filter=ACMRTUXB "$CHANGED_DIFF_REF" 2>/dev/null || true
129163
git diff --name-only --diff-filter=ACMRTUXB -- 2>/dev/null || true
130164
git ls-files --others --exclude-standard
131165
} | awk '/^(src|extensions|apps|packages|scripts)\// { print }' | sort -u
@@ -135,7 +169,7 @@ if (( PATHS_PASSED == 0 )); then
135169
RULEPACK_CHANGED_PATHS+=( "$path" )
136170
done < <(
137171
{
138-
git diff --name-only --diff-filter=ACMRTUXB "${OPENCLAW_OPENGREP_BASE_REF:-origin/main...HEAD}" 2>/dev/null || true
172+
git diff --name-only --diff-filter=ACMRTUXB "$CHANGED_DIFF_REF" 2>/dev/null || true
139173
git diff --name-only --diff-filter=ACMRTUXB -- 2>/dev/null || true
140174
git ls-files --others --exclude-standard
141175
} | awk '/^(security\/opengrep\/|scripts\/run-opengrep\.sh$|\.semgrepignore$|\.github\/workflows\/opengrep-)/ { print }' | sort -u

src/commands/migrate/skill-selection-prompt.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,5 +256,5 @@ export function promptMigrationSkillSelectionValues(
256256
return prompt.prompt();
257257
}
258258

259-
/** Back-compat alias for plugin selection prompts that share the same picker. */
259+
/** Compatibility alias for plugin selection prompts that share the same picker. */
260260
export const promptMigrationSelectionValues = promptMigrationSkillSelectionValues;

src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -515,8 +515,10 @@ function collectDeprecatedTestBarrelImports(): string[] {
515515
function collectDeprecatedPackageTestingBridgeDrift(): string[] {
516516
const source = fs
517517
.readFileSync(resolve(REPO_ROOT, "packages/plugin-sdk/src/testing.ts"), "utf8")
518-
.trim();
519-
return source === 'export * from "../../../src/plugin-sdk/testing.js";'
518+
.split("\n")
519+
.map((line) => line.trim())
520+
.filter((line) => line && !line.startsWith("//"));
521+
return source.length === 1 && source[0] === 'export * from "../../../src/plugin-sdk/testing.js";'
520522
? []
521523
: ["packages/plugin-sdk/src/testing.ts"];
522524
}

0 commit comments

Comments
 (0)