@blogic-cz/agent-tools 1.6.17 → 1.6.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -2
- package/dist/credential-guard/index.d.ts.map +1 -1
- package/dist/credential-guard/passive-jq.d.ts +5 -0
- package/dist/credential-guard/passive-jq.d.ts.map +1 -0
- package/dist/gh-tool/pr/commands.d.ts +1 -0
- package/dist/gh-tool/pr/commands.d.ts.map +1 -1
- package/dist/gh-tool/pr/core.d.ts +1 -0
- package/dist/gh-tool/pr/core.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/credential-guard/index.ts +590 -74
- package/src/credential-guard/passive-jq.ts +159 -0
- package/src/gh-tool/pr/commands.ts +6 -1
- package/src/gh-tool/pr/core.ts +60 -8
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/** Closed admission grammar, not an evaluator. Unknown syntax never acquires a proof. */
|
|
2
|
+
export function isPassiveJqFilter(filter: string): boolean {
|
|
3
|
+
if (filter.length > 65_536) return false;
|
|
4
|
+
if (new TextEncoder().encode(filter).byteLength > 65_536) return false;
|
|
5
|
+
const tokens: string[] = [];
|
|
6
|
+
const lexer =
|
|
7
|
+
/\s+|"(?:[^"\\\x00-\x1f]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"|(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?|[A-Za-z_][A-Za-z0-9_]*|\/\/|==|!=|<=|>=|[.{}[\]():,?|+*/%<>-]/y; // eslint-disable-line eslint/no-control-regex -- JSON strings exclude literal control bytes.
|
|
8
|
+
let offset = 0;
|
|
9
|
+
while (offset < filter.length) {
|
|
10
|
+
lexer.lastIndex = offset;
|
|
11
|
+
const match = lexer.exec(filter);
|
|
12
|
+
if (!match) return false;
|
|
13
|
+
offset = lexer.lastIndex;
|
|
14
|
+
if (!/^\s/.test(match[0])) tokens.push(match[0]);
|
|
15
|
+
if (tokens.length > 4096) return false;
|
|
16
|
+
}
|
|
17
|
+
let position = 0;
|
|
18
|
+
const take = (token: string): boolean => {
|
|
19
|
+
if (tokens[position] !== token) return false;
|
|
20
|
+
position++;
|
|
21
|
+
return true;
|
|
22
|
+
};
|
|
23
|
+
const identifier = (token: string | undefined): boolean =>
|
|
24
|
+
/^[A-Za-z_][A-Za-z0-9_]*$/.test(token ?? "");
|
|
25
|
+
const string = (token: string | undefined): boolean => token?.startsWith('"') === true;
|
|
26
|
+
const number = (token: string | undefined): boolean => /^[0-9]/.test(token ?? "");
|
|
27
|
+
const noArgs = new Set([
|
|
28
|
+
"length",
|
|
29
|
+
"keys",
|
|
30
|
+
"keys_unsorted",
|
|
31
|
+
"type",
|
|
32
|
+
"values",
|
|
33
|
+
"empty",
|
|
34
|
+
"add",
|
|
35
|
+
"min",
|
|
36
|
+
"max",
|
|
37
|
+
"sort",
|
|
38
|
+
"unique",
|
|
39
|
+
"flatten",
|
|
40
|
+
"not",
|
|
41
|
+
"reverse",
|
|
42
|
+
"to_entries",
|
|
43
|
+
"from_entries",
|
|
44
|
+
]);
|
|
45
|
+
const oneArg = new Set([
|
|
46
|
+
"map",
|
|
47
|
+
"map_values",
|
|
48
|
+
"select",
|
|
49
|
+
"contains",
|
|
50
|
+
"has",
|
|
51
|
+
"sort_by",
|
|
52
|
+
"group_by",
|
|
53
|
+
"unique_by",
|
|
54
|
+
"any",
|
|
55
|
+
"all",
|
|
56
|
+
"join",
|
|
57
|
+
]);
|
|
58
|
+
const operators = new Set([
|
|
59
|
+
"|",
|
|
60
|
+
"//",
|
|
61
|
+
"==",
|
|
62
|
+
"!=",
|
|
63
|
+
"<",
|
|
64
|
+
">",
|
|
65
|
+
"<=",
|
|
66
|
+
">=",
|
|
67
|
+
"+",
|
|
68
|
+
"-",
|
|
69
|
+
"*",
|
|
70
|
+
"/",
|
|
71
|
+
"%",
|
|
72
|
+
"and",
|
|
73
|
+
"or",
|
|
74
|
+
]);
|
|
75
|
+
const expression = (depth: number, commas = false): boolean => {
|
|
76
|
+
if (depth > 64 || !term(depth)) return false;
|
|
77
|
+
const nextOperator = () =>
|
|
78
|
+
operators.has(tokens[position] ?? "") || (commas && tokens[position] === ",");
|
|
79
|
+
while (nextOperator()) {
|
|
80
|
+
position++;
|
|
81
|
+
if (!term(depth)) return false;
|
|
82
|
+
}
|
|
83
|
+
return true;
|
|
84
|
+
};
|
|
85
|
+
const term = (depth: number): boolean => {
|
|
86
|
+
if (take("-")) return number(tokens[position]) && (position++, true);
|
|
87
|
+
if (take(".")) {
|
|
88
|
+
if (identifier(tokens[position]) || string(tokens[position])) position++;
|
|
89
|
+
} else if (take("(")) {
|
|
90
|
+
if (!expression(depth + 1, true) || !take(")")) return false;
|
|
91
|
+
} else if (take("[")) {
|
|
92
|
+
if (!take("]") && (!expression(depth + 1, true) || !take("]"))) return false;
|
|
93
|
+
} else if (take("{")) {
|
|
94
|
+
if (!take("}")) {
|
|
95
|
+
do {
|
|
96
|
+
const key = tokens[position];
|
|
97
|
+
if (!identifier(key) && !string(key)) return false;
|
|
98
|
+
position++;
|
|
99
|
+
if (take(":")) {
|
|
100
|
+
if (!expression(depth + 1)) return false;
|
|
101
|
+
} else if (!identifier(key)) return false;
|
|
102
|
+
} while (take(","));
|
|
103
|
+
if (!take("}")) return false;
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
const token = tokens[position];
|
|
107
|
+
if (string(token) || number(token) || ["true", "false", "null"].includes(token ?? ""))
|
|
108
|
+
position++;
|
|
109
|
+
else if (noArgs.has(token ?? "")) position++;
|
|
110
|
+
else if (oneArg.has(token ?? "")) {
|
|
111
|
+
position++;
|
|
112
|
+
if (!take("(") || !expression(depth + 1) || !take(")")) return false;
|
|
113
|
+
} else return false;
|
|
114
|
+
}
|
|
115
|
+
while (true) {
|
|
116
|
+
if (take("?")) continue;
|
|
117
|
+
if (take(".")) {
|
|
118
|
+
if (!identifier(tokens[position]) && !string(tokens[position])) return false;
|
|
119
|
+
position++;
|
|
120
|
+
} else if (take("[")) {
|
|
121
|
+
if (!take("]")) {
|
|
122
|
+
if (take("-")) {
|
|
123
|
+
if (!number(tokens[position])) return false;
|
|
124
|
+
position++;
|
|
125
|
+
} else if (string(tokens[position]) || number(tokens[position])) position++;
|
|
126
|
+
else return false;
|
|
127
|
+
if (!take("]")) return false;
|
|
128
|
+
}
|
|
129
|
+
} else break;
|
|
130
|
+
}
|
|
131
|
+
return true;
|
|
132
|
+
};
|
|
133
|
+
return expression(0, true) && position === tokens.length;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Only known formatting/input flags and one literal program, followed by file operands. */
|
|
137
|
+
export function passiveJqProgramIndex(argv: string[]): number | undefined {
|
|
138
|
+
if (argv[0]?.split("/").at(-1) !== "jq") return undefined;
|
|
139
|
+
const longOptions = new Set([
|
|
140
|
+
"--raw-output",
|
|
141
|
+
"--raw-input",
|
|
142
|
+
"--compact-output",
|
|
143
|
+
"--slurp",
|
|
144
|
+
"--monochrome-output",
|
|
145
|
+
"--color-output",
|
|
146
|
+
"--null-input",
|
|
147
|
+
"--exit-status",
|
|
148
|
+
"--ascii-output",
|
|
149
|
+
"--join-output",
|
|
150
|
+
"--sort-keys",
|
|
151
|
+
"--unbuffered",
|
|
152
|
+
"--tab",
|
|
153
|
+
]);
|
|
154
|
+
let index = 1;
|
|
155
|
+
while (/^-[rRcMsScneaj]+$/.test(argv[index] ?? "") || longOptions.has(argv[index] ?? "")) index++;
|
|
156
|
+
if (argv[index] === "--") index++;
|
|
157
|
+
if (argv.slice(index + 1).some((arg) => arg.startsWith("-") && arg !== "-")) return undefined;
|
|
158
|
+
return isPassiveJqFilter(argv[index] ?? "") ? index : undefined;
|
|
159
|
+
}
|
|
@@ -747,6 +747,10 @@ export const prMergeCommand = Command.make(
|
|
|
747
747
|
Flag.withDefault(DEFAULT_DELETE_BRANCH),
|
|
748
748
|
),
|
|
749
749
|
format: formatOption,
|
|
750
|
+
matchHeadCommit: Flag.String("match-head-commit").pipe(
|
|
751
|
+
Flag.withDescription("Require the PR head to match this full reviewed commit SHA"),
|
|
752
|
+
Flag.optional,
|
|
753
|
+
),
|
|
750
754
|
pr: Flag.Int("pr").pipe(Flag.withDescription("PR number to merge")),
|
|
751
755
|
repo: repoOption,
|
|
752
756
|
strategy: Flag.Literals("strategy", MERGE_STRATEGIES).pipe(
|
|
@@ -754,13 +758,14 @@ export const prMergeCommand = Command.make(
|
|
|
754
758
|
Flag.withDefault(DEFAULT_MERGE_STRATEGY),
|
|
755
759
|
),
|
|
756
760
|
},
|
|
757
|
-
({ confirm, deleteBranch, format, pr, repo, strategy }) =>
|
|
761
|
+
({ confirm, deleteBranch, format, matchHeadCommit, pr, repo, strategy }) =>
|
|
758
762
|
withRepo(
|
|
759
763
|
repo,
|
|
760
764
|
Effect.gen(function* () {
|
|
761
765
|
const result = yield* mergePR({
|
|
762
766
|
confirm,
|
|
763
767
|
deleteBranch,
|
|
768
|
+
matchHeadCommit: Option.getOrUndefined(matchHeadCommit),
|
|
764
769
|
pr,
|
|
765
770
|
strategy,
|
|
766
771
|
});
|
package/src/gh-tool/pr/core.ts
CHANGED
|
@@ -43,6 +43,8 @@ const STABLE_SNAPSHOT_ATTEMPTS = 3;
|
|
|
43
43
|
const GITHUB_ACTIONS_RUN_ID_RE = /github\.com\/[^/]+\/[^/]+\/actions\/runs\/(\d+)/;
|
|
44
44
|
const MERGE_SHA_POLL_INTERVAL_MS = 1000;
|
|
45
45
|
const MERGE_SHA_WAIT_SECONDS = 5;
|
|
46
|
+
const isFullCommitSha = (value: unknown): value is string =>
|
|
47
|
+
typeof value === "string" && value.length === 40 && /^[0-9a-f]+$/i.test(value);
|
|
46
48
|
|
|
47
49
|
const validatePRTitle = Effect.fn("pr.validatePRTitle")(function* (title: string) {
|
|
48
50
|
const gh = yield* GitHubService;
|
|
@@ -930,16 +932,54 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
|
|
|
930
932
|
strategy: MergeStrategy;
|
|
931
933
|
deleteBranch: boolean;
|
|
932
934
|
confirm: boolean;
|
|
935
|
+
matchHeadCommit?: string;
|
|
933
936
|
}) {
|
|
934
937
|
const gh = yield* GitHubService;
|
|
938
|
+
const matchHeadCommit = opts.matchHeadCommit;
|
|
935
939
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
940
|
+
if (matchHeadCommit !== undefined && !isFullCommitSha(matchHeadCommit)) {
|
|
941
|
+
return yield* new GitHubMergeError({
|
|
942
|
+
message: "--match-head-commit must be a full 40-character hexadecimal commit SHA",
|
|
943
|
+
reason: "unknown",
|
|
944
|
+
hint: "Provide the complete reviewed head commit SHA before retrying.",
|
|
945
|
+
nextCommand: `agent-tools-gh pr view --pr ${opts.pr}`,
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const info = yield* gh
|
|
950
|
+
.runGhJson<PRViewJsonResult & { headRefOid?: unknown }>([
|
|
951
|
+
"pr",
|
|
952
|
+
"view",
|
|
953
|
+
String(opts.pr),
|
|
954
|
+
"--json",
|
|
955
|
+
"number,url,title,headRefName,baseRefName,state,isDraft,mergeable" +
|
|
956
|
+
(matchHeadCommit !== undefined ? ",headRefOid" : ""),
|
|
957
|
+
])
|
|
958
|
+
.pipe(
|
|
959
|
+
Effect.mapError((error) =>
|
|
960
|
+
matchHeadCommit === undefined
|
|
961
|
+
? error
|
|
962
|
+
: new GitHubMergeError({
|
|
963
|
+
message: `Could not verify the head commit of PR #${opts.pr}: ${error.message}`,
|
|
964
|
+
reason: "unknown",
|
|
965
|
+
hint: "The merge is refused while the head is unknown. Re-read the PR and verify its reviewed head before retrying.",
|
|
966
|
+
nextCommand: `agent-tools-gh pr view --pr ${opts.pr}`,
|
|
967
|
+
}),
|
|
968
|
+
),
|
|
969
|
+
);
|
|
970
|
+
|
|
971
|
+
if (
|
|
972
|
+
matchHeadCommit !== undefined &&
|
|
973
|
+
(!isFullCommitSha(info?.headRefOid) ||
|
|
974
|
+
info.headRefOid.toLowerCase() !== matchHeadCommit.toLowerCase())
|
|
975
|
+
) {
|
|
976
|
+
return yield* new GitHubMergeError({
|
|
977
|
+
message: `PR #${opts.pr} head does not match the reviewed commit ${matchHeadCommit}`,
|
|
978
|
+
reason: "unknown",
|
|
979
|
+
hint: "The head is missing, invalid, or changed. Re-read the PR and review its current head before retrying with that SHA.",
|
|
980
|
+
nextCommand: `agent-tools-gh pr view --pr ${opts.pr}`,
|
|
981
|
+
});
|
|
982
|
+
}
|
|
943
983
|
|
|
944
984
|
const repo = opts.deleteBranch ? yield* gh.getRepoInfo() : null;
|
|
945
985
|
|
|
@@ -1120,6 +1160,9 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
|
|
|
1120
1160
|
});
|
|
1121
1161
|
|
|
1122
1162
|
const mergeArgs = ["pr", "merge", String(opts.pr), `--${opts.strategy}`];
|
|
1163
|
+
if (matchHeadCommit !== undefined) {
|
|
1164
|
+
mergeArgs.push("--match-head-commit", matchHeadCommit);
|
|
1165
|
+
}
|
|
1123
1166
|
|
|
1124
1167
|
const shaFromAsyncMerge = yield* gh
|
|
1125
1168
|
.runGh(mergeArgs)
|
|
@@ -1127,7 +1170,16 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
|
|
|
1127
1170
|
Effect.map(() => null),
|
|
1128
1171
|
Effect.catchTag("GitHubCommandError", (error) =>
|
|
1129
1172
|
ASYNC_MERGE_REQUIRED_RE.test(error.stderr)
|
|
1130
|
-
?
|
|
1173
|
+
? matchHeadCommit !== undefined
|
|
1174
|
+
? Effect.fail(
|
|
1175
|
+
new GitHubMergeError({
|
|
1176
|
+
message: `PR #${opts.pr} requires an asynchronous merge that cannot enforce --match-head-commit`,
|
|
1177
|
+
reason: "unknown",
|
|
1178
|
+
hint: "The asynchronous fallback was refused because its exact-head condition is unsupported. Keep the reviewed SHA requirement and use a merge path that enforces it after reviewing the current head.",
|
|
1179
|
+
nextCommand: `agent-tools-gh pr view --pr ${opts.pr}`,
|
|
1180
|
+
}),
|
|
1181
|
+
)
|
|
1182
|
+
: mergeViaAsyncApi({ pr: opts.pr, strategy: opts.strategy })
|
|
1131
1183
|
: Effect.fail(error),
|
|
1132
1184
|
),
|
|
1133
1185
|
)
|