@generacy-ai/workflow-engine 0.5.0 → 0.7.0
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/dist/actions/github/client/ci-verdict.d.ts +22 -0
- package/dist/actions/github/client/ci-verdict.d.ts.map +1 -0
- package/dist/actions/github/client/ci-verdict.js +57 -0
- package/dist/actions/github/client/ci-verdict.js.map +1 -0
- package/dist/actions/github/client/gh-cli.d.ts +40 -2
- package/dist/actions/github/client/gh-cli.d.ts.map +1 -1
- package/dist/actions/github/client/gh-cli.js +312 -3
- package/dist/actions/github/client/gh-cli.js.map +1 -1
- package/dist/actions/github/client/interface.d.ts +113 -3
- package/dist/actions/github/client/interface.d.ts.map +1 -1
- package/dist/actions/github/label-definitions.d.ts.map +1 -1
- package/dist/actions/github/label-definitions.js +17 -0
- package/dist/actions/github/label-definitions.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/types/github.d.ts +59 -2
- package/dist/types/github.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CiRun, CiVerdict } from '../../../types/github.js';
|
|
2
|
+
/**
|
|
3
|
+
* Aggregate a set of CI runs for a single head SHA into a three-state verdict.
|
|
4
|
+
*
|
|
5
|
+
* Pure, total, and never throws. Zero I/O.
|
|
6
|
+
*
|
|
7
|
+
* Precedence (per contracts/ci-verdict.md), after dropping the ignore-set
|
|
8
|
+
* (`skipped`, `neutral`):
|
|
9
|
+
* 1. any failing terminal conclusion → `not-passed`
|
|
10
|
+
* 2. any in-progress run (`status !== 'completed'` or `conclusion === null`) → `pending`
|
|
11
|
+
* 3. any `success` → `green`
|
|
12
|
+
* 4. otherwise → `pending`
|
|
13
|
+
*
|
|
14
|
+
* Unknown terminal conclusions are treated conservatively: they are not
|
|
15
|
+
* `success`, so they never contribute to a `green` verdict, and they fall
|
|
16
|
+
* through to `pending` (rule 4) unless a concrete success exists.
|
|
17
|
+
*
|
|
18
|
+
* Invariants: empty input and all-ignored input both yield `pending` — never
|
|
19
|
+
* `green` (SC-001). A `green` verdict requires at least one concrete `success`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function aggregateCiVerdict(runs: CiRun[]): CiVerdict;
|
|
22
|
+
//# sourceMappingURL=ci-verdict.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ci-verdict.d.ts","sourceRoot":"","sources":["../../../../src/actions/github/client/ci-verdict.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAqBjE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,SAAS,CAgC3D"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conclusions that are ignored entirely when computing merge readiness.
|
|
3
|
+
* A skipped or neutral run is NOT a pass — it means CI never ran for that job
|
|
4
|
+
* (skipped≠passed, #1133 / SC-001).
|
|
5
|
+
*/
|
|
6
|
+
const IGNORED_CONCLUSIONS = new Set(['skipped', 'neutral']);
|
|
7
|
+
/**
|
|
8
|
+
* Terminal conclusions that block merge readiness.
|
|
9
|
+
*/
|
|
10
|
+
const FAILING_CONCLUSIONS = new Set([
|
|
11
|
+
'failure',
|
|
12
|
+
'cancelled',
|
|
13
|
+
'timed_out',
|
|
14
|
+
'action_required',
|
|
15
|
+
'startup_failure', // #1157 FR-006
|
|
16
|
+
'stale', // #1157 FR-006
|
|
17
|
+
]);
|
|
18
|
+
/**
|
|
19
|
+
* Aggregate a set of CI runs for a single head SHA into a three-state verdict.
|
|
20
|
+
*
|
|
21
|
+
* Pure, total, and never throws. Zero I/O.
|
|
22
|
+
*
|
|
23
|
+
* Precedence (per contracts/ci-verdict.md), after dropping the ignore-set
|
|
24
|
+
* (`skipped`, `neutral`):
|
|
25
|
+
* 1. any failing terminal conclusion → `not-passed`
|
|
26
|
+
* 2. any in-progress run (`status !== 'completed'` or `conclusion === null`) → `pending`
|
|
27
|
+
* 3. any `success` → `green`
|
|
28
|
+
* 4. otherwise → `pending`
|
|
29
|
+
*
|
|
30
|
+
* Unknown terminal conclusions are treated conservatively: they are not
|
|
31
|
+
* `success`, so they never contribute to a `green` verdict, and they fall
|
|
32
|
+
* through to `pending` (rule 4) unless a concrete success exists.
|
|
33
|
+
*
|
|
34
|
+
* Invariants: empty input and all-ignored input both yield `pending` — never
|
|
35
|
+
* `green` (SC-001). A `green` verdict requires at least one concrete `success`.
|
|
36
|
+
*/
|
|
37
|
+
export function aggregateCiVerdict(runs) {
|
|
38
|
+
const relevant = runs.filter((run) => run.conclusion === null || !IGNORED_CONCLUSIONS.has(run.conclusion));
|
|
39
|
+
if (relevant.length === 0) {
|
|
40
|
+
return 'pending';
|
|
41
|
+
}
|
|
42
|
+
// Rule 1: any failing terminal conclusion blocks readiness.
|
|
43
|
+
if (relevant.some((run) => run.conclusion !== null && FAILING_CONCLUSIONS.has(run.conclusion))) {
|
|
44
|
+
return 'not-passed';
|
|
45
|
+
}
|
|
46
|
+
// Rule 2: any in-progress run keeps the verdict pending.
|
|
47
|
+
if (relevant.some((run) => run.status !== 'completed' || run.conclusion === null)) {
|
|
48
|
+
return 'pending';
|
|
49
|
+
}
|
|
50
|
+
// Rule 3: at least one concrete success and nothing failing/in-progress.
|
|
51
|
+
if (relevant.some((run) => run.conclusion === 'success')) {
|
|
52
|
+
return 'green';
|
|
53
|
+
}
|
|
54
|
+
// Rule 4: only unknown terminal conclusions remain — conservatively pending.
|
|
55
|
+
return 'pending';
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=ci-verdict.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ci-verdict.js","sourceRoot":"","sources":["../../../../src/actions/github/client/ci-verdict.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAS,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC;AAEpE;;GAEG;AACH,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAS;IAC1C,SAAS;IACT,WAAW;IACX,WAAW;IACX,iBAAiB;IACjB,iBAAiB,EAAE,eAAe;IAClC,OAAO,EAAE,eAAe;CACzB,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAa;IAC9C,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAC1B,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,KAAK,IAAI,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,CAC7E,CAAC;IAEF,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,4DAA4D;IAC5D,IACE,QAAQ,CAAC,IAAI,CACX,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,KAAK,IAAI,IAAI,mBAAmB,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,CAC5E,EACD,CAAC;QACD,OAAO,YAAY,CAAC;IACtB,CAAC;IAED,yDAAyD;IACzD,IACE,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,KAAK,WAAW,IAAI,GAAG,CAAC,UAAU,KAAK,IAAI,CAAC,EAC7E,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,yEAAyE;IACzE,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,KAAK,SAAS,CAAC,EAAE,CAAC;QACzD,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,6EAA6E;IAC7E,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Uses the GitHub CLI for all GitHub API operations and git for local operations.
|
|
4
4
|
*/
|
|
5
5
|
import type { GitHubClient, IssueUpdate, PRCreate, PRUpdate, MergeResult, CommitResult, PushResult, GitStatus, LabelDefinition } from './interface.js';
|
|
6
|
-
import type { Issue, PullRequest, Comment, Label, RepoInfo, Review, ReviewThread } from '../../../types/github.js';
|
|
6
|
+
import type { Issue, PullRequest, Comment, Label, RepoInfo, Review, ReviewThread, CreateReviewInput, PullRequestFile, CiRun } from '../../../types/github.js';
|
|
7
7
|
/**
|
|
8
8
|
* Thrown by `executeGh()` when the gh CLI's stderr signals HTTP 401 or 403.
|
|
9
9
|
* Callers (label/PR-feedback monitors) catch this to drive auth-health state.
|
|
@@ -59,6 +59,9 @@ export declare class GhCliGitHubClient implements GitHubClient {
|
|
|
59
59
|
getPRComments(owner: string, repo: string, number: number): Promise<Comment[]>;
|
|
60
60
|
getPRReviewThreads(owner: string, repo: string, number: number): Promise<ReviewThread[]>;
|
|
61
61
|
listReviews(owner: string, repo: string, prNumber: number): Promise<Review[]>;
|
|
62
|
+
createReview(owner: string, repo: string, prNumber: number, input: CreateReviewInput): Promise<Review>;
|
|
63
|
+
convertPullRequestToDraft(owner: string, repo: string, prNumber: number): Promise<void>;
|
|
64
|
+
listPullRequestFiles(owner: string, repo: string, prNumber: number): Promise<PullRequestFile[]>;
|
|
62
65
|
replyToPRComment(owner: string, repo: string, number: number, commentId: number, body: string): Promise<Comment>;
|
|
63
66
|
resolveReviewThread(threadId: string): Promise<void>;
|
|
64
67
|
listOpenPullRequests(owner: string, repo: string): Promise<PullRequest[]>;
|
|
@@ -79,7 +82,7 @@ export declare class GhCliGitHubClient implements GitHubClient {
|
|
|
79
82
|
checkout(branch: string): Promise<void>;
|
|
80
83
|
stageFiles(files: string[]): Promise<void>;
|
|
81
84
|
stageAll(): Promise<void>;
|
|
82
|
-
commit(message: string): Promise<CommitResult>;
|
|
85
|
+
commit(message: string, pathspec?: string[]): Promise<CommitResult>;
|
|
83
86
|
push(remote?: string, branch?: string, setUpstream?: boolean): Promise<PushResult>;
|
|
84
87
|
fetch(remote?: string, prune?: boolean): Promise<void>;
|
|
85
88
|
merge(branch: string, noCommit?: boolean): Promise<MergeResult>;
|
|
@@ -89,6 +92,7 @@ export declare class GhCliGitHubClient implements GitHubClient {
|
|
|
89
92
|
success: boolean;
|
|
90
93
|
conflicts: boolean;
|
|
91
94
|
}>;
|
|
95
|
+
discardWorkingTreeChanges(excludePaths?: string[]): Promise<void>;
|
|
92
96
|
getConflictedFiles(): Promise<string[]>;
|
|
93
97
|
getDefaultBranch(): Promise<string>;
|
|
94
98
|
getCommitsBetween(base: string, head: string): Promise<{
|
|
@@ -96,6 +100,9 @@ export declare class GhCliGitHubClient implements GitHubClient {
|
|
|
96
100
|
message: string;
|
|
97
101
|
}[]>;
|
|
98
102
|
getFilesChangedBetween(base: string, head: string): Promise<string[]>;
|
|
103
|
+
getCurrentCommitSha(): Promise<string>;
|
|
104
|
+
getFilesChangedByOwnCommits(startRef: string): Promise<string[]>;
|
|
105
|
+
commitExistsInCheckout(sha: string): Promise<boolean>;
|
|
99
106
|
/**
|
|
100
107
|
* Fetch the head commit SHA for a branch/ref (#892).
|
|
101
108
|
* Uses `gh api repos/{o}/{r}/commits/{ref} --jq .sha` and validates that the
|
|
@@ -103,6 +110,37 @@ export declare class GhCliGitHubClient implements GitHubClient {
|
|
|
103
110
|
* 401 → `GhAuthError` via `executeGh` (existing #762 path).
|
|
104
111
|
*/
|
|
105
112
|
getRefHeadSha(owner: string, repo: string, ref: string): Promise<string>;
|
|
113
|
+
/**
|
|
114
|
+
* Read CI runs for a commit SHA for merge-readiness aggregation (#1133).
|
|
115
|
+
*
|
|
116
|
+
* Primary: `gh api repos/{o}/{r}/commits/{sha}/check-runs` → source
|
|
117
|
+
* `check-runs`. On non-zero exit — including the `GhAuthError` (HTTP 401/403)
|
|
118
|
+
* that `executeGh` raises, which is the observed symptom of a token lacking
|
|
119
|
+
* `checks:read` (FR-002) — fall back to
|
|
120
|
+
* `gh api repos/{o}/{r}/actions/runs?branch={branch}` filtered client-side to
|
|
121
|
+
* the head SHA → source `actions-runs`.
|
|
122
|
+
*
|
|
123
|
+
* Both paths are paginated (`--paginate` + `per_page=100`): the check-runs
|
|
124
|
+
* endpoint caps at 30 results per page by default (same trap #1043 fixed for
|
|
125
|
+
* `listBranches`). Without pagination a head SHA with >30 checks would expose
|
|
126
|
+
* only page 1 to `aggregateCiVerdict`, so a failing or still-pending run past
|
|
127
|
+
* page 1 would be invisible and could yield a false `green` — the exact
|
|
128
|
+
* skipped≠passed safety hole this feature closes.
|
|
129
|
+
*
|
|
130
|
+
* Both paths normalize to `CiRun[]` consumable by `aggregateCiVerdict`
|
|
131
|
+
* unchanged (SC-004). Empty result → `{ runs: [], source }`. Non-zero exit on
|
|
132
|
+
* BOTH paths → throw with stderr (mirrors `getRefHeadSha`).
|
|
133
|
+
*/
|
|
134
|
+
getCiRunsForSha(owner: string, repo: string, headSha: string, branch: string): Promise<{
|
|
135
|
+
runs: CiRun[];
|
|
136
|
+
source: 'check-runs' | 'actions-runs';
|
|
137
|
+
}>;
|
|
138
|
+
/**
|
|
139
|
+
* Parse newline-delimited `{status, conclusion}` JSON objects (jq stream
|
|
140
|
+
* output) into `CiRun[]`. Blank lines and unparseable lines are skipped.
|
|
141
|
+
* Unknown `conclusion` values are passed through as-is.
|
|
142
|
+
*/
|
|
143
|
+
private parseCiRunLines;
|
|
106
144
|
/**
|
|
107
145
|
* List file names touched by a PR (`gh pr diff --name-only`, #892).
|
|
108
146
|
* Non-zero exit → throw with stderr for visibility.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gh-cli.d.ts","sourceRoot":"","sources":["../../../../src/actions/github/client/gh-cli.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EACV,YAAY,EACZ,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,YAAY,EACZ,UAAU,EACV,SAAS,EACT,eAAe,EAChB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EACV,KAAK,EACL,WAAW,EACX,OAAO,EACP,KAAK,EACL,QAAQ,EAER,MAAM,EAEN,YAAY,
|
|
1
|
+
{"version":3,"file":"gh-cli.d.ts","sourceRoot":"","sources":["../../../../src/actions/github/client/gh-cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { executeCommand, parseJSONSafe } from '../../cli-utils.js';
|
|
2
|
+
import { writeFile, unlink } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
2
6
|
/**
|
|
3
7
|
* Thrown by `executeGh()` when the gh CLI's stderr signals HTTP 401 or 403.
|
|
4
8
|
* Callers (label/PR-feedback monitors) catch this to drive auth-health state.
|
|
@@ -573,6 +577,141 @@ export class GhCliGitHubClient {
|
|
|
573
577
|
}
|
|
574
578
|
return reviews;
|
|
575
579
|
}
|
|
580
|
+
async createReview(owner, repo, prNumber, input) {
|
|
581
|
+
// The reviews endpoint accepts a nested `comments[]` array of objects,
|
|
582
|
+
// which `-f field=value` cannot express. The shared `executeCommand`
|
|
583
|
+
// wrapper ignores stdin (both the launcher and direct-spawn paths use
|
|
584
|
+
// `stdio: ['ignore', ...]`), so `--input -` is unavailable here. Write the
|
|
585
|
+
// JSON body to a temp file and pass `--input <file>` — same wire result.
|
|
586
|
+
const body = {
|
|
587
|
+
event: input.event,
|
|
588
|
+
body: input.body,
|
|
589
|
+
comments: (input.comments ?? []).map(c => ({
|
|
590
|
+
path: c.path,
|
|
591
|
+
line: c.line,
|
|
592
|
+
side: c.side ?? 'RIGHT',
|
|
593
|
+
body: c.body,
|
|
594
|
+
})),
|
|
595
|
+
};
|
|
596
|
+
const inputPath = join(tmpdir(), `generacy-review-${randomUUID()}.json`);
|
|
597
|
+
await writeFile(inputPath, JSON.stringify(body), 'utf8');
|
|
598
|
+
let result;
|
|
599
|
+
try {
|
|
600
|
+
result = await this.executeGh([
|
|
601
|
+
'api',
|
|
602
|
+
'--method', 'POST',
|
|
603
|
+
`/repos/${owner}/${repo}/pulls/${prNumber}/reviews`,
|
|
604
|
+
'--input', inputPath,
|
|
605
|
+
]);
|
|
606
|
+
}
|
|
607
|
+
finally {
|
|
608
|
+
await unlink(inputPath).catch(() => {
|
|
609
|
+
/* best-effort cleanup */
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
if (result.exitCode !== 0) {
|
|
613
|
+
throw new Error(`Failed to create review on PR #${prNumber}: ${result.stderr}`);
|
|
614
|
+
}
|
|
615
|
+
const data = parseJSONSafe(result.stdout);
|
|
616
|
+
if (!data) {
|
|
617
|
+
throw new Error(`Failed to parse created review response on PR #${prNumber}`);
|
|
618
|
+
}
|
|
619
|
+
const review = {
|
|
620
|
+
id: data.id,
|
|
621
|
+
user: { login: data.user?.login ?? '' },
|
|
622
|
+
body: data.body ?? '',
|
|
623
|
+
state: data.state,
|
|
624
|
+
submittedAt: data.submitted_at,
|
|
625
|
+
};
|
|
626
|
+
if (typeof data.author_association === 'string' && data.author_association.length > 0) {
|
|
627
|
+
review.authorAssociation = data.author_association;
|
|
628
|
+
}
|
|
629
|
+
return review;
|
|
630
|
+
}
|
|
631
|
+
async convertPullRequestToDraft(owner, repo, prNumber) {
|
|
632
|
+
// Step 1 — resolve the PR node id + current draft state. If it is already
|
|
633
|
+
// a draft, the conversion is a no-op (idempotent short-circuit).
|
|
634
|
+
const query = 'query($owner: String!, $repo: String!, $n: Int!) { repository(owner: $owner, name: $repo) { pullRequest(number: $n) { id isDraft } } }';
|
|
635
|
+
const queryResult = await this.executeGh([
|
|
636
|
+
'api', 'graphql',
|
|
637
|
+
'-f', `query=${query}`,
|
|
638
|
+
'-F', `owner=${owner}`,
|
|
639
|
+
'-F', `repo=${repo}`,
|
|
640
|
+
'-F', `n=${prNumber}`,
|
|
641
|
+
]);
|
|
642
|
+
if (queryResult.exitCode !== 0) {
|
|
643
|
+
throw new Error(`convertPullRequestToDraft failed to resolve PR #${prNumber}: ${queryResult.stderr}`);
|
|
644
|
+
}
|
|
645
|
+
const queryParsed = parseJSONSafe(queryResult.stdout);
|
|
646
|
+
if (queryParsed?.errors && queryParsed.errors.length > 0) {
|
|
647
|
+
const messages = queryParsed.errors.map(e => e.message ?? 'unknown').join('; ');
|
|
648
|
+
throw new Error(`convertPullRequestToDraft returned GraphQL errors resolving PR #${prNumber}: ${messages}`);
|
|
649
|
+
}
|
|
650
|
+
const pr = queryParsed?.data?.repository?.pullRequest;
|
|
651
|
+
if (!pr?.id) {
|
|
652
|
+
throw new Error(`convertPullRequestToDraft could not resolve node id for PR #${prNumber}`);
|
|
653
|
+
}
|
|
654
|
+
if (pr.isDraft === true)
|
|
655
|
+
return;
|
|
656
|
+
// Step 2 — run the mutation, mirroring `resolveReviewThread`'s retry/auth
|
|
657
|
+
// handling: 3× backoff, rethrow GhAuthError, terminal on GraphQL errors[].
|
|
658
|
+
const mutation = 'mutation($id: ID!) { convertPullRequestToDraft(input: { pullRequestId: $id }) { pullRequest { id isDraft } } }';
|
|
659
|
+
const backoffs = [1000, 2000, 4000];
|
|
660
|
+
let lastError = null;
|
|
661
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
662
|
+
try {
|
|
663
|
+
const result = await this.executeGh([
|
|
664
|
+
'api', 'graphql',
|
|
665
|
+
'-f', `query=${mutation}`,
|
|
666
|
+
'-F', `id=${pr.id}`,
|
|
667
|
+
]);
|
|
668
|
+
if (result.exitCode !== 0) {
|
|
669
|
+
lastError = new Error(`convertPullRequestToDraft mutation failed for PR #${prNumber}: ${result.stderr}`);
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
const parsed = parseJSONSafe(result.stdout);
|
|
673
|
+
if (parsed?.errors && parsed.errors.length > 0) {
|
|
674
|
+
const messages = parsed.errors.map(e => e.message ?? 'unknown').join('; ');
|
|
675
|
+
throw new Error(`convertPullRequestToDraft returned GraphQL errors for PR #${prNumber}: ${messages}`);
|
|
676
|
+
}
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
catch (err) {
|
|
681
|
+
if (err instanceof GhAuthError)
|
|
682
|
+
throw err;
|
|
683
|
+
if (err instanceof Error &&
|
|
684
|
+
err.message.startsWith('convertPullRequestToDraft returned GraphQL errors')) {
|
|
685
|
+
throw err;
|
|
686
|
+
}
|
|
687
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
688
|
+
}
|
|
689
|
+
const backoff = backoffs[attempt];
|
|
690
|
+
if (backoff !== undefined) {
|
|
691
|
+
await new Promise(resolve => setTimeout(resolve, backoff));
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
throw lastError ?? new Error(`convertPullRequestToDraft failed for PR #${prNumber}`);
|
|
695
|
+
}
|
|
696
|
+
async listPullRequestFiles(owner, repo, prNumber) {
|
|
697
|
+
const result = await this.executeGh([
|
|
698
|
+
'api',
|
|
699
|
+
`/repos/${owner}/${repo}/pulls/${prNumber}/files?per_page=100`,
|
|
700
|
+
'--paginate',
|
|
701
|
+
]);
|
|
702
|
+
if (result.exitCode !== 0) {
|
|
703
|
+
throw new Error(`Failed to list files for PR #${prNumber}: ${result.stderr}`);
|
|
704
|
+
}
|
|
705
|
+
const data = parseJSONSafe(result.stdout);
|
|
706
|
+
if (!data)
|
|
707
|
+
return [];
|
|
708
|
+
return data.map(f => {
|
|
709
|
+
const file = { filename: f.filename, status: f.status };
|
|
710
|
+
if (typeof f.patch === 'string')
|
|
711
|
+
file.patch = f.patch;
|
|
712
|
+
return file;
|
|
713
|
+
});
|
|
714
|
+
}
|
|
576
715
|
async replyToPRComment(owner, repo, number, commentId, body) {
|
|
577
716
|
const result = await this.executeGh([
|
|
578
717
|
'api',
|
|
@@ -879,7 +1018,12 @@ export class GhCliGitHubClient {
|
|
|
879
1018
|
async getStatus() {
|
|
880
1019
|
const branchResult = await executeCommand('git', ['branch', '--show-current'], { cwd: this.workdir });
|
|
881
1020
|
const branch = branchResult.stdout.trim();
|
|
882
|
-
|
|
1021
|
+
// `--untracked-files=all`: report every untracked file individually. The
|
|
1022
|
+
// default collapses a wholly-untracked directory into one `?? dir/` entry,
|
|
1023
|
+
// which hides the files inside from path-based consumers (e.g. the
|
|
1024
|
+
// orchestrator's engine-sidecar staging filter, #1162 — a bare `.generacy/`
|
|
1025
|
+
// entry would be staged wholesale, committing every sidecar).
|
|
1026
|
+
const statusResult = await executeCommand('git', ['status', '--porcelain', '--untracked-files=all'], { cwd: this.workdir });
|
|
883
1027
|
const lines = statusResult.stdout.split('\n').filter(l => l);
|
|
884
1028
|
const staged = [];
|
|
885
1029
|
const unstaged = [];
|
|
@@ -975,8 +1119,15 @@ export class GhCliGitHubClient {
|
|
|
975
1119
|
throw new Error(`Failed to stage all: ${result.stderr}`);
|
|
976
1120
|
}
|
|
977
1121
|
}
|
|
978
|
-
async commit(message) {
|
|
979
|
-
|
|
1122
|
+
async commit(message, pathspec) {
|
|
1123
|
+
// With an explicit pathspec, `git commit -m <msg> -- <paths>` records only
|
|
1124
|
+
// those paths and disregards anything else staged in the index (#1162) —
|
|
1125
|
+
// the caller must have already staged untracked members of `pathspec`.
|
|
1126
|
+
// Without it, the whole index is committed (legacy behavior).
|
|
1127
|
+
const args = pathspec && pathspec.length > 0
|
|
1128
|
+
? ['commit', '-m', message, '--', ...pathspec]
|
|
1129
|
+
: ['commit', '-m', message];
|
|
1130
|
+
const result = await executeCommand('git', args, { cwd: this.workdir });
|
|
980
1131
|
if (result.exitCode !== 0) {
|
|
981
1132
|
throw new Error(`Failed to commit: ${result.stderr}`);
|
|
982
1133
|
}
|
|
@@ -1089,6 +1240,23 @@ export class GhCliGitHubClient {
|
|
|
1089
1240
|
}
|
|
1090
1241
|
return { success: true, conflicts: false };
|
|
1091
1242
|
}
|
|
1243
|
+
async discardWorkingTreeChanges(excludePaths = []) {
|
|
1244
|
+
// Revert tracked modifications (staged + unstaged) to HEAD.
|
|
1245
|
+
const reset = await executeCommand('git', ['reset', '--hard', 'HEAD'], { cwd: this.workdir });
|
|
1246
|
+
if (reset.exitCode !== 0) {
|
|
1247
|
+
throw new Error(`Failed to reset working tree: ${reset.stderr}`);
|
|
1248
|
+
}
|
|
1249
|
+
// Remove untracked files/directories left behind by the abandoned work.
|
|
1250
|
+
// `-e <pattern>` keeps caller-owned untracked state (e.g. `.generacy/`).
|
|
1251
|
+
const cleanArgs = ['clean', '-fd'];
|
|
1252
|
+
for (const pattern of excludePaths) {
|
|
1253
|
+
cleanArgs.push('-e', pattern);
|
|
1254
|
+
}
|
|
1255
|
+
const clean = await executeCommand('git', cleanArgs, { cwd: this.workdir });
|
|
1256
|
+
if (clean.exitCode !== 0) {
|
|
1257
|
+
throw new Error(`Failed to clean working tree: ${clean.stderr}`);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1092
1260
|
async getConflictedFiles() {
|
|
1093
1261
|
const result = await executeCommand('git', ['diff', '--name-only', '--diff-filter=U'], { cwd: this.workdir });
|
|
1094
1262
|
return result.stdout.split('\n').filter(f => f);
|
|
@@ -1132,6 +1300,44 @@ export class GhCliGitHubClient {
|
|
|
1132
1300
|
}
|
|
1133
1301
|
return result.stdout.split('\n').filter(Boolean);
|
|
1134
1302
|
}
|
|
1303
|
+
async getCurrentCommitSha() {
|
|
1304
|
+
const result = await executeCommand('git', [
|
|
1305
|
+
'rev-parse', 'HEAD',
|
|
1306
|
+
], { cwd: this.workdir });
|
|
1307
|
+
if (result.exitCode !== 0) {
|
|
1308
|
+
throw new Error(`git rev-parse HEAD failed (exit ${result.exitCode}): ${result.stderr.trim()}`);
|
|
1309
|
+
}
|
|
1310
|
+
return result.stdout.trim();
|
|
1311
|
+
}
|
|
1312
|
+
async getFilesChangedByOwnCommits(startRef) {
|
|
1313
|
+
const result = await executeCommand('git', [
|
|
1314
|
+
'log', '--first-parent', '--no-merges', '--name-only', '--pretty=format:',
|
|
1315
|
+
`${startRef}..HEAD`,
|
|
1316
|
+
], { cwd: this.workdir });
|
|
1317
|
+
if (result.exitCode !== 0) {
|
|
1318
|
+
throw new Error(`git log --first-parent --no-merges --name-only ${startRef}..HEAD failed ` +
|
|
1319
|
+
`(exit ${result.exitCode}): ${result.stderr.trim()}`);
|
|
1320
|
+
}
|
|
1321
|
+
const seen = new Set();
|
|
1322
|
+
for (const line of result.stdout.split('\n')) {
|
|
1323
|
+
const path = line.trim();
|
|
1324
|
+
if (path) {
|
|
1325
|
+
seen.add(path);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
return [...seen];
|
|
1329
|
+
}
|
|
1330
|
+
async commitExistsInCheckout(sha) {
|
|
1331
|
+
const result = await executeCommand('git', [
|
|
1332
|
+
'rev-parse', '--verify', '--quiet', `${sha}^{commit}`,
|
|
1333
|
+
], { cwd: this.workdir });
|
|
1334
|
+
if (result.exitCode === 0)
|
|
1335
|
+
return true;
|
|
1336
|
+
if (result.exitCode === 1)
|
|
1337
|
+
return false; // commit-missing (FR-003, Q4=B)
|
|
1338
|
+
throw new Error(`git rev-parse --verify --quiet ${sha}^{commit} failed ` +
|
|
1339
|
+
`(exit ${result.exitCode}): ${result.stderr.trim()}`);
|
|
1340
|
+
}
|
|
1135
1341
|
// ==========================================================================
|
|
1136
1342
|
// Alias Methods (convenience wrappers)
|
|
1137
1343
|
// ==========================================================================
|
|
@@ -1156,6 +1362,109 @@ export class GhCliGitHubClient {
|
|
|
1156
1362
|
}
|
|
1157
1363
|
return sha;
|
|
1158
1364
|
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Read CI runs for a commit SHA for merge-readiness aggregation (#1133).
|
|
1367
|
+
*
|
|
1368
|
+
* Primary: `gh api repos/{o}/{r}/commits/{sha}/check-runs` → source
|
|
1369
|
+
* `check-runs`. On non-zero exit — including the `GhAuthError` (HTTP 401/403)
|
|
1370
|
+
* that `executeGh` raises, which is the observed symptom of a token lacking
|
|
1371
|
+
* `checks:read` (FR-002) — fall back to
|
|
1372
|
+
* `gh api repos/{o}/{r}/actions/runs?branch={branch}` filtered client-side to
|
|
1373
|
+
* the head SHA → source `actions-runs`.
|
|
1374
|
+
*
|
|
1375
|
+
* Both paths are paginated (`--paginate` + `per_page=100`): the check-runs
|
|
1376
|
+
* endpoint caps at 30 results per page by default (same trap #1043 fixed for
|
|
1377
|
+
* `listBranches`). Without pagination a head SHA with >30 checks would expose
|
|
1378
|
+
* only page 1 to `aggregateCiVerdict`, so a failing or still-pending run past
|
|
1379
|
+
* page 1 would be invisible and could yield a false `green` — the exact
|
|
1380
|
+
* skipped≠passed safety hole this feature closes.
|
|
1381
|
+
*
|
|
1382
|
+
* Both paths normalize to `CiRun[]` consumable by `aggregateCiVerdict`
|
|
1383
|
+
* unchanged (SC-004). Empty result → `{ runs: [], source }`. Non-zero exit on
|
|
1384
|
+
* BOTH paths → throw with stderr (mirrors `getRefHeadSha`).
|
|
1385
|
+
*/
|
|
1386
|
+
async getCiRunsForSha(owner, repo, headSha, branch) {
|
|
1387
|
+
let primaryError = '';
|
|
1388
|
+
try {
|
|
1389
|
+
const primary = await this.executeGh([
|
|
1390
|
+
'api',
|
|
1391
|
+
'--paginate',
|
|
1392
|
+
`repos/${owner}/${repo}/commits/${headSha}/check-runs?per_page=100`,
|
|
1393
|
+
'--jq', '.check_runs[] | {status, conclusion}',
|
|
1394
|
+
]);
|
|
1395
|
+
if (primary.exitCode === 0) {
|
|
1396
|
+
return {
|
|
1397
|
+
runs: this.parseCiRunLines(primary.stdout),
|
|
1398
|
+
source: 'check-runs',
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
primaryError = `exit ${primary.exitCode}: ${primary.stderr.trim()}`;
|
|
1402
|
+
}
|
|
1403
|
+
catch (err) {
|
|
1404
|
+
// executeGh throws GhAuthError on 401/403 — the checks:read-missing
|
|
1405
|
+
// symptom. Treat it as a fallback trigger, not a fatal readout error.
|
|
1406
|
+
primaryError = err instanceof Error ? err.message : String(err);
|
|
1407
|
+
}
|
|
1408
|
+
// #1157 FR-007: this fallback only enumerates GitHub-Actions `workflow_runs`
|
|
1409
|
+
// for the branch (filtered client-side to the head SHA). It is BLIND to
|
|
1410
|
+
// third-party required checks (external status contexts), so a `green`
|
|
1411
|
+
// aggregated from these runs may be a false green. The primary path is used
|
|
1412
|
+
// only when `check-runs` failed — the canonical symptom of a token lacking
|
|
1413
|
+
// `checks:read`. To close the false-green hole, `evaluateCiReadiness`
|
|
1414
|
+
// (packages/orchestrator/src/worker/ci-merge-readiness.ts) fails-closed:
|
|
1415
|
+
// when `source === 'actions-runs'` a would-be `green` is downgraded to
|
|
1416
|
+
// `not-passed`. Operator note: a `checks:read`-lacking cluster fails closed
|
|
1417
|
+
// (CI merge readiness never reports green via this fallback); granting the
|
|
1418
|
+
// token `checks:read` restores full third-party-check visibility via the
|
|
1419
|
+
// primary `check-runs` path.
|
|
1420
|
+
const fallback = await this.executeGh([
|
|
1421
|
+
'api',
|
|
1422
|
+
'--paginate',
|
|
1423
|
+
`repos/${owner}/${repo}/actions/runs?branch=${branch}&per_page=100`,
|
|
1424
|
+
'--jq', '.workflow_runs[] | {head_sha, status, conclusion}',
|
|
1425
|
+
]);
|
|
1426
|
+
if (fallback.exitCode !== 0) {
|
|
1427
|
+
throw new Error(`getCiRunsForSha ${owner}/${repo}@${headSha} failed on both paths ` +
|
|
1428
|
+
`(check-runs ${primaryError}; ` +
|
|
1429
|
+
`actions-runs exit ${fallback.exitCode}: ${fallback.stderr.trim()})`);
|
|
1430
|
+
}
|
|
1431
|
+
// The actions/runs jq keeps head_sha, so filter to the target SHA.
|
|
1432
|
+
const filtered = [];
|
|
1433
|
+
for (const line of fallback.stdout.split('\n')) {
|
|
1434
|
+
const trimmed = line.trim();
|
|
1435
|
+
if (!trimmed)
|
|
1436
|
+
continue;
|
|
1437
|
+
const parsed = parseJSONSafe(trimmed);
|
|
1438
|
+
if (!parsed || parsed.head_sha !== headSha)
|
|
1439
|
+
continue;
|
|
1440
|
+
filtered.push({
|
|
1441
|
+
status: typeof parsed.status === 'string' ? parsed.status : '',
|
|
1442
|
+
conclusion: (parsed.conclusion ?? null),
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
return { runs: filtered, source: 'actions-runs' };
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* Parse newline-delimited `{status, conclusion}` JSON objects (jq stream
|
|
1449
|
+
* output) into `CiRun[]`. Blank lines and unparseable lines are skipped.
|
|
1450
|
+
* Unknown `conclusion` values are passed through as-is.
|
|
1451
|
+
*/
|
|
1452
|
+
parseCiRunLines(stdout) {
|
|
1453
|
+
const runs = [];
|
|
1454
|
+
for (const line of stdout.split('\n')) {
|
|
1455
|
+
const trimmed = line.trim();
|
|
1456
|
+
if (!trimmed)
|
|
1457
|
+
continue;
|
|
1458
|
+
const parsed = parseJSONSafe(trimmed);
|
|
1459
|
+
if (!parsed)
|
|
1460
|
+
continue;
|
|
1461
|
+
runs.push({
|
|
1462
|
+
status: typeof parsed.status === 'string' ? parsed.status : '',
|
|
1463
|
+
conclusion: (parsed.conclusion ?? null),
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
return runs;
|
|
1467
|
+
}
|
|
1159
1468
|
/**
|
|
1160
1469
|
* List file names touched by a PR (`gh pr diff --name-only`, #892).
|
|
1161
1470
|
* Non-zero exit → throw with stderr for visibility.
|