@blogic-cz/agent-tools 0.18.0 → 0.18.1

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.
@@ -30,7 +30,7 @@ export declare class GitHubAuthError extends GitHubAuthError_base {
30
30
  }
31
31
  declare const GitHubMergeError_base: Schema.Class<GitHubMergeError, Schema.TaggedStruct<"GitHubMergeError", {
32
32
  readonly message: Schema.String;
33
- readonly reason: Schema.Literals<readonly ["conflicts", "checks_failing", "branch_protected", "unknown"]>;
33
+ readonly reason: Schema.Literals<readonly ["conflicts", "checks_failing", "branch_protected", "merge_queue", "unknown"]>;
34
34
  readonly hint: Schema.optionalKey<Schema.String>;
35
35
  readonly nextCommand: Schema.optionalKey<Schema.String>;
36
36
  readonly retryable: Schema.optionalKey<Schema.Boolean>;
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/gh-tool/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;;;;;;;;;;AAEhC,qBAAa,kBAAmB,SAAQ,uBAWvC;CAAG;;;;;;;;;AAEJ,qBAAa,mBAAoB,SAAQ,wBAUxC;CAAG;;;;;;;AAEJ,qBAAa,eAAgB,SAAQ,oBAKnC;CAAG;;;;;;;;AAEL,qBAAa,gBAAiB,SAAQ,qBAMpC;CAAG;;;;;;;;AAEL,qBAAa,kBAAmB,SAAQ,uBASvC;CAAG;AAEJ,MAAM,MAAM,kBAAkB,GAC1B,kBAAkB,GAClB,mBAAmB,GACnB,eAAe,GACf,gBAAgB,GAChB,kBAAkB,CAAC"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/gh-tool/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;;;;;;;;;;AAEhC,qBAAa,kBAAmB,SAAQ,uBAWvC;CAAG;;;;;;;;;AAEJ,qBAAa,mBAAoB,SAAQ,wBAUxC;CAAG;;;;;;;AAEJ,qBAAa,eAAgB,SAAQ,oBAKnC;CAAG;;;;;;;;AAEL,qBAAa,gBAAiB,SAAQ,qBAYpC;CAAG;;;;;;;;AAEL,qBAAa,kBAAmB,SAAQ,uBASvC;CAAG;AAEJ,MAAM,MAAM,kBAAkB,GAC1B,kBAAkB,GAClB,mBAAmB,GACnB,eAAe,GACf,gBAAgB,GAChB,kBAAkB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../../src/gh-tool/pr/core.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../../src/gh-tool/pr/core.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blogic-cz/agent-tools",
3
- "version": "0.18.0",
3
+ "version": "0.18.1",
4
4
  "description": "CLI tools for AI coding agent workflows — GitHub, database, Kubernetes, Azure DevOps, logs, sessions, and audit",
5
5
  "keywords": [
6
6
  "agent",
@@ -34,7 +34,13 @@ export class GitHubAuthError extends Schema.TaggedError<GitHubAuthError>()("GitH
34
34
 
35
35
  export class GitHubMergeError extends Schema.TaggedError<GitHubMergeError>()("GitHubMergeError", {
36
36
  message: Schema.String,
37
- reason: Schema.Literals(["conflicts", "checks_failing", "branch_protected", "unknown"]),
37
+ reason: Schema.Literals([
38
+ "conflicts",
39
+ "checks_failing",
40
+ "branch_protected",
41
+ "merge_queue",
42
+ "unknown",
43
+ ]),
38
44
  hint: Schema.optionalKey(Schema.String),
39
45
  nextCommand: Schema.optionalKey(Schema.String),
40
46
  retryable: Schema.optionalKey(Schema.Boolean),
@@ -702,6 +702,99 @@ export const createPR = Effect.fn("pr.createPR")(function* (opts: {
702
702
  );
703
703
  });
704
704
 
705
+ // GitHub rejects GraphQL `mergePullRequest` for a PR that belongs to a native stack and points at
706
+ // the asynchronous REST merge instead. `gh pr merge` only speaks GraphQL.
707
+ const ASYNC_MERGE_REQUIRED_RE = /asynchronous merge/i;
708
+ const ASYNC_MERGE_POLL_INTERVAL_MS = 2000;
709
+ const MAX_ASYNC_MERGE_WAIT_SECONDS = 120;
710
+
711
+ type AsyncMergeResult = {
712
+ status: "pending" | "merged" | "enqueued" | "failed";
713
+ details?: { message?: string; uuid?: string; sha?: string };
714
+ };
715
+
716
+ const asyncMergeFailure = (
717
+ pr: number,
718
+ message: string,
719
+ hint: string,
720
+ reason: "merge_queue" | "unknown" = "unknown",
721
+ ) =>
722
+ new GitHubMergeError({
723
+ message: `Failed to merge PR #${pr} asynchronously: ${message}`,
724
+ reason,
725
+ hint,
726
+ nextCommand: `agent-tools-gh pr view --pr ${pr}`,
727
+ });
728
+
729
+ const mergeViaAsyncApi = Effect.fn("pr.mergeViaAsyncApi")(function* (opts: {
730
+ pr: number;
731
+ strategy: MergeStrategy;
732
+ }) {
733
+ const gh = yield* GitHubService;
734
+ const repo = yield* gh.getRepoInfo();
735
+ const asyncPath = `repos/${repo.owner}/${repo.name}/pulls/${opts.pr}/merge-async`;
736
+
737
+ let latest = yield* gh.runGhJson<AsyncMergeResult>([
738
+ "api",
739
+ "--method",
740
+ "PUT",
741
+ asyncPath,
742
+ "-f",
743
+ `merge_method=${opts.strategy}`,
744
+ ]);
745
+
746
+ const uuid = latest.details?.uuid;
747
+ if (latest.status === "pending" && uuid !== undefined) {
748
+ const start = yield* Clock.currentTimeMillis;
749
+ const deadlineMs = Number(start) + MAX_ASYNC_MERGE_WAIT_SECONDS * 1000;
750
+ let timedOut = false;
751
+
752
+ // Effect.whileLoop (not recursion) so TestClock.adjust can advance Effect.sleep without real waits.
753
+ yield* Effect.whileLoop({
754
+ while: () => latest.status === "pending" && !timedOut,
755
+ body: () =>
756
+ Effect.gen(function* () {
757
+ const now = yield* Clock.currentTimeMillis;
758
+ if (Number(now) >= deadlineMs) {
759
+ timedOut = true;
760
+ return;
761
+ }
762
+ const remaining = deadlineMs - Number(now);
763
+ yield* Effect.sleep(Duration.millis(Math.min(ASYNC_MERGE_POLL_INTERVAL_MS, remaining)));
764
+ latest = yield* gh.runGhJson<AsyncMergeResult>(["api", `${asyncPath}/${uuid}`]);
765
+ }),
766
+ step: () => undefined,
767
+ });
768
+ }
769
+
770
+ if (latest.status === "merged") {
771
+ return latest.details?.sha ?? null;
772
+ }
773
+
774
+ if (latest.status === "enqueued") {
775
+ return yield* asyncMergeFailure(
776
+ opts.pr,
777
+ latest.details?.message ?? "the pull request entered a merge queue",
778
+ "The merge queue owns the merge from here; it is not merged yet. Watch the PR until the queue drains.",
779
+ "merge_queue",
780
+ );
781
+ }
782
+
783
+ if (latest.status === "pending") {
784
+ return yield* asyncMergeFailure(
785
+ opts.pr,
786
+ `still pending after ${MAX_ASYNC_MERGE_WAIT_SECONDS}s`,
787
+ "The asynchronous merge is still running. Re-check the PR state before retrying so the merge is not requested twice.",
788
+ );
789
+ }
790
+
791
+ return yield* asyncMergeFailure(
792
+ opts.pr,
793
+ latest.details?.message ?? "the merge request failed",
794
+ "Inspect the PR state and branch protections, then retry.",
795
+ );
796
+ });
797
+
705
798
  export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
706
799
  pr: number;
707
800
  strategy: MergeStrategy;
@@ -847,77 +940,92 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
847
940
 
848
941
  const mergeArgs = ["pr", "merge", String(opts.pr), `--${opts.strategy}`];
849
942
 
850
- const mergeResult = yield* gh.runGh(mergeArgs).pipe(
851
- Effect.catch((error) =>
852
- rollbackRetargets.pipe(
853
- Effect.andThen(
854
- (
855
- rollbackFailed,
856
- ): Effect.Effect<never, GitHubNotFoundError | GitHubAuthError | GitHubMergeError> => {
857
- const rollbackNote =
858
- rollbackFailed.length > 0
859
- ? ` ROLLBACK INCOMPLETE: dependent PR(s) ${rollbackFailed
860
- .map((child) => `#${child}`)
861
- .join(", ")} are still retargeted to \`${info.baseRefName}\`; ` +
862
- `manually restore their base to \`${info.headRefName}\`.`
863
- : "";
864
-
865
- if (error._tag !== "GitHubCommandError") {
866
- return rollbackNote === ""
867
- ? Effect.fail(error)
868
- : Console.error(rollbackNote.trim()).pipe(Effect.andThen(Effect.fail(error)));
869
- }
870
-
871
- const stderr = error.stderr.toLowerCase();
872
-
873
- if (stderr.includes("merge conflict") || stderr.includes("conflicts")) {
874
- return Effect.fail(
875
- new GitHubMergeError({
876
- message: `PR #${opts.pr} has merge conflicts`,
877
- reason: "conflicts",
878
- hint: `Resolve merge conflicts locally, push the fix, then retry the merge.${rollbackNote}`,
879
- nextCommand: `gh pr diff ${opts.pr}`,
880
- }),
881
- );
882
- }
883
-
884
- if (stderr.includes("required status check") || stderr.includes("checks")) {
885
- return Effect.fail(
886
- new GitHubMergeError({
887
- message: `PR #${opts.pr} has failing required checks`,
888
- reason: "checks_failing",
889
- hint: `Wait for CI checks to pass or investigate failures before merging.${rollbackNote}`,
890
- nextCommand: `agent-tools-gh pr checks --pr ${opts.pr}`,
891
- retryable: true,
892
- }),
893
- );
894
- }
895
-
896
- if (stderr.includes("protected branch")) {
897
- return Effect.fail(
898
- new GitHubMergeError({
899
- message: `PR #${opts.pr} targets a protected branch`,
900
- reason: "branch_protected",
901
- hint: `This branch has protection rules. Ensure required reviews and checks are satisfied, or ask a repo admin.${rollbackNote}`,
902
- }),
903
- );
904
- }
905
-
906
- return Effect.fail(
907
- new GitHubMergeError({
908
- message: `Failed to merge PR #${opts.pr}: ${error.stderr}`,
909
- reason: "unknown",
910
- hint: `Check the PR state and branch protections. The PR may already be merged or closed.${rollbackNote}`,
911
- nextCommand: `agent-tools-gh pr view --pr ${opts.pr}`,
912
- }),
913
- );
914
- },
915
- ),
943
+ const mergedSha = yield* gh
944
+ .runGh(mergeArgs)
945
+ .pipe(
946
+ Effect.map((result) => result.stdout.match(/([0-9a-f]{7,40})/)?.[1] ?? null),
947
+ Effect.catchTag("GitHubCommandError", (error) =>
948
+ ASYNC_MERGE_REQUIRED_RE.test(error.stderr)
949
+ ? mergeViaAsyncApi({ pr: opts.pr, strategy: opts.strategy })
950
+ : Effect.fail(error),
916
951
  ),
917
- ),
918
- );
919
-
920
- const shaMatch = mergeResult.stdout.match(/([0-9a-f]{7,40})/);
952
+ )
953
+ .pipe(
954
+ Effect.catch((error) =>
955
+ // A queued merge still lands, and its head branch disappears with it. Rolling the
956
+ // dependents back onto that branch would close them (cli/cli#1168), so keep the retargets.
957
+ error._tag === "GitHubMergeError" && error.reason === "merge_queue"
958
+ ? Effect.fail(error)
959
+ : rollbackRetargets.pipe(
960
+ Effect.andThen(
961
+ (
962
+ rollbackFailed,
963
+ ): Effect.Effect<
964
+ never,
965
+ GitHubNotFoundError | GitHubAuthError | GitHubMergeError
966
+ > => {
967
+ const rollbackNote =
968
+ rollbackFailed.length > 0
969
+ ? ` ROLLBACK INCOMPLETE: dependent PR(s) ${rollbackFailed
970
+ .map((child) => `#${child}`)
971
+ .join(", ")} are still retargeted to \`${info.baseRefName}\`; ` +
972
+ `manually restore their base to \`${info.headRefName}\`.`
973
+ : "";
974
+
975
+ if (error._tag !== "GitHubCommandError") {
976
+ return rollbackNote === ""
977
+ ? Effect.fail(error)
978
+ : Console.error(rollbackNote.trim()).pipe(Effect.andThen(Effect.fail(error)));
979
+ }
980
+
981
+ const stderr = error.stderr.toLowerCase();
982
+
983
+ if (stderr.includes("merge conflict") || stderr.includes("conflicts")) {
984
+ return Effect.fail(
985
+ new GitHubMergeError({
986
+ message: `PR #${opts.pr} has merge conflicts`,
987
+ reason: "conflicts",
988
+ hint: `Resolve merge conflicts locally, push the fix, then retry the merge.${rollbackNote}`,
989
+ nextCommand: `gh pr diff ${opts.pr}`,
990
+ }),
991
+ );
992
+ }
993
+
994
+ if (stderr.includes("required status check") || stderr.includes("checks")) {
995
+ return Effect.fail(
996
+ new GitHubMergeError({
997
+ message: `PR #${opts.pr} has failing required checks`,
998
+ reason: "checks_failing",
999
+ hint: `Wait for CI checks to pass or investigate failures before merging.${rollbackNote}`,
1000
+ nextCommand: `agent-tools-gh pr checks --pr ${opts.pr}`,
1001
+ retryable: true,
1002
+ }),
1003
+ );
1004
+ }
1005
+
1006
+ if (stderr.includes("protected branch")) {
1007
+ return Effect.fail(
1008
+ new GitHubMergeError({
1009
+ message: `PR #${opts.pr} targets a protected branch`,
1010
+ reason: "branch_protected",
1011
+ hint: `This branch has protection rules. Ensure required reviews and checks are satisfied, or ask a repo admin.${rollbackNote}`,
1012
+ }),
1013
+ );
1014
+ }
1015
+
1016
+ return Effect.fail(
1017
+ new GitHubMergeError({
1018
+ message: `Failed to merge PR #${opts.pr}: ${error.stderr}`,
1019
+ reason: "unknown",
1020
+ hint: `Check the PR state and branch protections. The PR may already be merged or closed.${rollbackNote}`,
1021
+ nextCommand: `agent-tools-gh pr view --pr ${opts.pr}`,
1022
+ }),
1023
+ );
1024
+ },
1025
+ ),
1026
+ ),
1027
+ ),
1028
+ );
921
1029
 
922
1030
  // `gh pr merge --delete-branch` aborts its remote delete when the head branch is checked out in a
923
1031
  // worktree; deleting the remote ref explicitly is worktree-independent. Local cleanup is separate.
@@ -944,7 +1052,7 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
944
1052
  merged: true,
945
1053
  strategy: opts.strategy,
946
1054
  branchDeleted: willDeleteBranch,
947
- sha: shaMatch?.[1] ?? null,
1055
+ sha: mergedSha,
948
1056
  retargetedChildren: retargetedChildren.length > 0 ? retargetedChildren : undefined,
949
1057
  branchDeleteSkipped: branchDeleteSkipped ? true : undefined,
950
1058
  };