@fitlab-ai/agent-infra 0.9.12 → 0.9.13
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 +11 -11
- package/README.zh-CN.md +11 -11
- package/dist/bin/internal-cli.js +24 -19
- package/dist/lib/agent-clients/next-steps.js +6 -1
- package/dist/lib/decide.js +3 -3
- package/dist/lib/internal/platform-checks.js +3 -3
- package/dist/lib/internal/platform-comment.js +9 -9
- package/dist/lib/internal/platform-context.js +2 -2
- package/dist/lib/internal/platform-issue.js +8 -9
- package/dist/lib/internal/platform-pr-review.js +10 -10
- package/dist/lib/internal/platform-pr.js +63 -24
- package/dist/lib/internal/platform-release-notes.js +3 -3
- package/dist/lib/internal/pr-review-grade.js +11 -9
- package/dist/lib/internal/release-workflow.js +4 -4
- package/dist/lib/internal/sandbox-control.js +30 -4
- package/dist/lib/internal/task-activity.js +3 -3
- package/dist/lib/internal/task-artifact.js +76 -24
- package/dist/lib/internal/task-create.js +2 -2
- package/dist/lib/internal/task-event.js +6 -3
- package/dist/lib/internal/task-finalization.js +2 -2
- package/dist/lib/internal/task-invalidation.js +60 -0
- package/dist/lib/internal/task-ledger.js +9 -6
- package/dist/lib/internal/task-lifecycle.js +2 -2
- package/dist/lib/internal/task-override.js +4 -4
- package/dist/lib/internal/task-review.js +3 -3
- package/dist/lib/internal/task-verify.js +6 -6
- package/dist/lib/internal/task-warning.js +3 -3
- package/dist/lib/platform/adapters.js +1 -3
- package/dist/lib/platform/change-request-git-evidence.js +27 -7
- package/dist/lib/platform/completion-backfill.js +6 -5
- package/dist/lib/platform/context.js +70 -130
- package/dist/lib/platform/github-client.js +1 -1
- package/dist/lib/platform/github-provider.js +903 -0
- package/dist/lib/platform/in-label-sync.js +143 -0
- package/dist/lib/platform/issue-comments.js +94 -41
- package/dist/lib/platform/issues.js +335 -189
- package/dist/lib/platform/metadata-labels.js +6 -2
- package/dist/lib/platform/none-provider.js +28 -0
- package/dist/lib/platform/pr-change-report.js +551 -0
- package/dist/lib/platform/pr-checks.js +119 -104
- package/dist/lib/platform/pr-review.js +88 -72
- package/dist/lib/platform/pr-summary.js +520 -50
- package/dist/lib/platform/provider-bridge.js +45 -0
- package/dist/lib/platform/provider-contract.d.ts +487 -0
- package/dist/lib/platform/provider-contract.js +95 -0
- package/dist/lib/platform/provider-loader.js +251 -0
- package/dist/lib/platform/provider-validation.js +504 -0
- package/dist/lib/platform/pull-request-metadata.js +8 -6
- package/dist/lib/platform/pull-requests.js +783 -537
- package/dist/lib/platform/release-notes.js +78 -55
- package/dist/lib/platform/releases.js +79 -50
- package/dist/lib/platform/resource-identity.d.ts +26 -0
- package/dist/lib/platform/resource-identity.js +92 -0
- package/dist/lib/platform/task-identities.js +31 -0
- package/dist/lib/platform/verification-required.js +11 -12
- package/dist/lib/platform/verification-sync.js +112 -238
- package/dist/lib/pr-review/evidence-grading.js +19 -6
- package/dist/lib/run/index.js +63 -23
- package/dist/lib/sandbox/control/client.js +96 -15
- package/dist/lib/sandbox/control/executor.js +3 -3
- package/dist/lib/sandbox/control/protocol.js +77 -0
- package/dist/lib/sandbox/control/server.js +398 -25
- package/dist/lib/sandbox/control/state.js +204 -1
- package/dist/lib/server/protocol.js +38 -1
- package/dist/lib/task/activity-log.js +8 -1
- package/dist/lib/task/artifact-lifecycle.js +33 -5
- package/dist/lib/task/capabilities.js +239 -0
- package/dist/lib/task/commands/status.js +32 -0
- package/dist/lib/task/commit-operation.js +12 -1
- package/dist/lib/task/control-authority.js +8 -5
- package/dist/lib/task/create-service.js +8 -8
- package/dist/lib/task/current-contract.js +5 -1
- package/dist/lib/task/decision-details.js +1 -1
- package/dist/lib/task/events.js +297 -17
- package/dist/lib/task/finalization.js +37 -13
- package/dist/lib/task/human-override.js +58 -36
- package/dist/lib/task/invalidation-command.js +86 -0
- package/dist/lib/task/invalidation.js +260 -0
- package/dist/lib/task/ledger-intents.js +37 -0
- package/dist/lib/task/local-artifact-finalization.js +374 -0
- package/dist/lib/task/orchestration.js +49 -93
- package/dist/lib/task/pr-delivery-fact.js +102 -101
- package/dist/lib/task/review-fingerprint.js +24 -2
- package/dist/lib/task/rework-intent.js +138 -0
- package/dist/lib/task/skill-scope.js +23 -0
- package/dist/lib/task/task-execution-lock.js +27 -17
- package/dist/lib/task/verification-engine.js +43 -16
- package/dist/lib/task/verification.js +3 -3
- package/dist/lib/task/workflow-warning-intents.js +1 -1
- package/dist/lib/task/write.js +11 -0
- package/dist/lib/version.js +11 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/next-steps.ts +6 -1
- package/lib/decide.ts +3 -3
- package/lib/init.ts +1 -1
- package/lib/internal/platform-checks.ts +3 -3
- package/lib/internal/platform-comment.ts +8 -8
- package/lib/internal/platform-context.ts +2 -2
- package/lib/internal/platform-issue.ts +7 -8
- package/lib/internal/platform-pr-review.ts +9 -9
- package/lib/internal/platform-pr.ts +47 -22
- package/lib/internal/platform-release-notes.ts +3 -3
- package/lib/internal/pr-review-grade.ts +10 -8
- package/lib/internal/release-workflow.ts +4 -4
- package/lib/internal/sandbox-control.ts +27 -3
- package/lib/internal/task-activity.ts +3 -3
- package/lib/internal/task-artifact.ts +65 -22
- package/lib/internal/task-create.ts +2 -2
- package/lib/internal/task-event.ts +6 -3
- package/lib/internal/task-finalization.ts +3 -3
- package/lib/internal/task-invalidation.ts +39 -0
- package/lib/internal/task-ledger.ts +11 -8
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-override.ts +6 -6
- package/lib/internal/task-review.ts +3 -3
- package/lib/internal/task-verify.ts +6 -6
- package/lib/internal/task-warning.ts +3 -3
- package/lib/platform/adapters.ts +3 -2
- package/lib/platform/change-request-git-evidence.ts +36 -9
- package/lib/platform/completion-backfill.ts +9 -8
- package/lib/platform/context.ts +97 -134
- package/lib/platform/github-client.ts +2 -2
- package/lib/platform/github-provider.ts +910 -0
- package/lib/platform/in-label-sync.ts +208 -0
- package/lib/platform/issue-comments.ts +97 -47
- package/lib/platform/issues.ts +344 -174
- package/lib/platform/metadata-labels.ts +5 -2
- package/lib/platform/none-provider.ts +35 -0
- package/lib/platform/pr-change-report.ts +645 -0
- package/lib/platform/pr-checks.ts +126 -106
- package/lib/platform/pr-review.ts +88 -85
- package/lib/platform/pr-summary.ts +560 -53
- package/lib/platform/provider-bridge.ts +82 -0
- package/lib/platform/provider-contract.ts +536 -0
- package/lib/platform/provider-loader.ts +307 -0
- package/lib/platform/provider-validation.ts +529 -0
- package/lib/platform/pull-request-metadata.ts +10 -7
- package/lib/platform/pull-requests.ts +826 -543
- package/lib/platform/release-notes.ts +76 -59
- package/lib/platform/releases.ts +77 -43
- package/lib/platform/resource-identity.ts +121 -0
- package/lib/platform/task-identities.ts +40 -0
- package/lib/platform/types.ts +4 -2
- package/lib/platform/verification-required.ts +11 -13
- package/lib/platform/verification-sync.ts +112 -292
- package/lib/pr-review/evidence-grading.ts +25 -8
- package/lib/run/index.ts +57 -22
- package/lib/sandbox/control/client.ts +105 -15
- package/lib/sandbox/control/executor.ts +4 -4
- package/lib/sandbox/control/protocol.ts +113 -0
- package/lib/sandbox/control/server.ts +431 -23
- package/lib/sandbox/control/state.ts +251 -1
- package/lib/server/protocol.ts +36 -1
- package/lib/task/activity-log.ts +8 -0
- package/lib/task/artifact-lifecycle.ts +32 -6
- package/lib/task/capabilities.ts +262 -0
- package/lib/task/commands/status.ts +38 -0
- package/lib/task/commit-operation.ts +12 -1
- package/lib/task/control-authority.ts +11 -8
- package/lib/task/create-service.ts +8 -8
- package/lib/task/current-contract.ts +6 -1
- package/lib/task/decision-details.ts +6 -2
- package/lib/task/events.ts +296 -16
- package/lib/task/finalization.ts +50 -14
- package/lib/task/human-override.ts +70 -46
- package/lib/task/invalidation-command.ts +86 -0
- package/lib/task/invalidation.ts +332 -0
- package/lib/task/ledger-intents.ts +35 -1
- package/lib/task/local-artifact-finalization.ts +563 -0
- package/lib/task/orchestration.ts +52 -85
- package/lib/task/pr-delivery-fact.ts +131 -148
- package/lib/task/review-fingerprint.ts +23 -2
- package/lib/task/rework-intent.ts +165 -0
- package/lib/task/skill-scope.ts +24 -0
- package/lib/task/task-execution-lock.ts +23 -14
- package/lib/task/verification-engine.ts +52 -17
- package/lib/task/verification.ts +4 -4
- package/lib/task/workflow-warning-intents.ts +1 -1
- package/lib/task/write.ts +19 -0
- package/lib/update.ts +1 -1
- package/lib/version.ts +15 -1
- package/package.json +7 -1
- package/templates/.agents/rules/README.en.md +1 -1
- package/templates/.agents/rules/README.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-pr-commands.en.md +1 -1
- package/templates/.agents/rules/issue-pr-commands.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.en.md +2 -0
- package/templates/.agents/rules/issue-sync.zh-CN.md +2 -0
- package/templates/.agents/rules/local-artifact-repair.en.md +9 -2
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +9 -2
- package/templates/.agents/rules/pr-sync.en.md +10 -4
- package/templates/.agents/rules/pr-sync.zh-CN.md +17 -4
- package/templates/.agents/rules/task-short-id.en.md +1 -4
- package/templates/.agents/rules/task-short-id.zh-CN.md +1 -3
- package/templates/.agents/skills/analyze-task/SKILL.en.md +13 -3
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +10 -3
- package/templates/.agents/skills/check-task/SKILL.en.md +16 -37
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +16 -37
- package/templates/.agents/skills/code-task/SKILL.en.md +27 -8
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +29 -10
- package/templates/.agents/skills/code-task/config/verify.en.json +1 -0
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +2 -0
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +2 -0
- package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +3 -3
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +3 -3
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +5 -3
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +5 -3
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +4 -3
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +4 -3
- package/templates/.agents/skills/complete-task/SKILL.en.md +21 -3
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +20 -3
- package/templates/.agents/skills/create-pr/SKILL.en.md +7 -6
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +7 -7
- package/templates/.agents/skills/create-pr/config/verify.json +1 -0
- package/templates/.agents/skills/create-pr/reference/change-report.en.md +16 -4
- package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +16 -4
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +5 -3
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +5 -3
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/scripts/change-report.mjs +3 -0
- package/templates/.agents/skills/plan-task/SKILL.en.md +13 -3
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +10 -3
- package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/review-plan/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +4 -2
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +4 -2
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +6 -0
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +6 -0
- package/templates/.agents/templates/task.en.md +18 -1
- package/templates/.agents/templates/task.zh-CN.md +18 -1
- package/templates/.github/workflows/pr-label.yml +7 -26
|
@@ -6,17 +6,21 @@ import { parseTypedTaskFrontmatter } from '../task/frontmatter.ts';
|
|
|
6
6
|
import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
7
7
|
import { extractSection } from '../task/sections.ts';
|
|
8
8
|
import { captureTaskWriteMetadata, writeTask } from '../task/write.ts';
|
|
9
|
-
import {
|
|
10
|
-
inspectPlatformChangeRequest,
|
|
11
|
-
inspectPlatformIssueClosingChangeRequests,
|
|
12
|
-
registerPlatformCapabilities
|
|
13
|
-
} from './adapters.ts';
|
|
14
9
|
import type { PlatformChangeRequestSnapshot } from './adapters.ts';
|
|
15
|
-
import { parseGitHubRemote, resolvePlatformContext } from './context.ts';
|
|
10
|
+
import { parseGitHubRemote, resolvePlatformContext, resolvePlatformProviderContext } from './context.ts';
|
|
11
|
+
import type { PlatformClient } from './context.ts';
|
|
16
12
|
import { createGitHubClient } from './github-client.ts';
|
|
17
|
-
import type { GitHubClient } from './github-client.ts';
|
|
18
13
|
import { inspectPlatformIssue } from './issues.ts';
|
|
14
|
+
import { inspectGitHubIssue } from './issues.ts';
|
|
15
|
+
import type { IssueSnapshot } from './issues.ts';
|
|
19
16
|
import { planPullRequestMetadata } from './pull-request-metadata.ts';
|
|
17
|
+
import {
|
|
18
|
+
extractPullRequestFileNames,
|
|
19
|
+
planInLabelUpdate,
|
|
20
|
+
syncLabelDelta,
|
|
21
|
+
validateInLabelMapping,
|
|
22
|
+
validateRepositoryLabelPayload
|
|
23
|
+
} from './in-label-sync.ts';
|
|
20
24
|
import { platformResult } from './types.ts';
|
|
21
25
|
import type { PlatformOperation, PlatformResult } from './types.ts';
|
|
22
26
|
import { inspectCompletionArtifacts } from '../task/finalization-artifacts.ts';
|
|
@@ -30,6 +34,19 @@ import {
|
|
|
30
34
|
readPrDeliveryFact
|
|
31
35
|
} from '../task/pr-delivery-fact.ts';
|
|
32
36
|
import type { CreationOutcome, PrDeliveryBindingSource, PrDeliveryFact } from '../task/pr-delivery-fact.ts';
|
|
37
|
+
import {
|
|
38
|
+
providerError,
|
|
39
|
+
providerOperationContext,
|
|
40
|
+
providerStatus,
|
|
41
|
+
providerResourceToken,
|
|
42
|
+
providerResourceIdentity,
|
|
43
|
+
resourceIdentity,
|
|
44
|
+
unsupportedProviderOperation
|
|
45
|
+
} from './provider-bridge.ts';
|
|
46
|
+
import { isResourceIdentity, resourceIdentityEquals, resourceIdentityNumber } from './resource-identity.ts';
|
|
47
|
+
import type { ResourceIdentity } from './resource-identity.ts';
|
|
48
|
+
import { taskIssueIdentity, taskIssueIdentityError } from './task-identities.ts';
|
|
49
|
+
import type { ChangeRequestSnapshot as ProviderChangeRequestSnapshot } from './provider-contract.ts';
|
|
33
50
|
|
|
34
51
|
type PullRequestSnapshot = PlatformChangeRequestSnapshot;
|
|
35
52
|
|
|
@@ -39,9 +56,19 @@ type PullRequestResult = PlatformResult & {
|
|
|
39
56
|
result: 'pr_created' | 'pr_reused' | 'no_op' | 'pr_created_with_warnings' | 'pr_reused_with_warnings' | 'no_op_with_warnings' | 'failed' | 'blocked' | null;
|
|
40
57
|
creation: CreationOutcome | null;
|
|
41
58
|
warnings: readonly OperationWarning[];
|
|
59
|
+
evidence?: { kind: string; pullRequestFiles?: string[]; closingIssues?: number[] };
|
|
60
|
+
resources?: Array<{
|
|
61
|
+
kind: 'issue' | 'pull-request';
|
|
62
|
+
number: number;
|
|
63
|
+
before: string[];
|
|
64
|
+
expected: string[];
|
|
65
|
+
after: string[] | null;
|
|
66
|
+
effect: 'no-op' | 'applied' | 'unknown';
|
|
67
|
+
}>;
|
|
42
68
|
};
|
|
43
|
-
type
|
|
44
|
-
type
|
|
69
|
+
type GitHubClient = PlatformClient;
|
|
70
|
+
type InspectionOptions = { cwd?: string; client?: PlatformClient; runtimeVersion?: string };
|
|
71
|
+
type SharedOptions = { cwd?: string; client?: PlatformClient; runtimeVersion?: string };
|
|
45
72
|
type CreateOptions = SharedOptions & {
|
|
46
73
|
agent: string;
|
|
47
74
|
base: string;
|
|
@@ -52,11 +79,11 @@ type CreateOptions = SharedOptions & {
|
|
|
52
79
|
dryRun?: boolean;
|
|
53
80
|
phase?: { value: CreatePhase };
|
|
54
81
|
};
|
|
55
|
-
type BindOptions = SharedOptions & { agent: string; pr: number; dryRun?: boolean };
|
|
82
|
+
type BindOptions = SharedOptions & { agent: string; pr: string | number; dryRun?: boolean };
|
|
56
83
|
type PullRequestPrimaryResult = 'pr_created' | 'pr_reused' | 'no_op';
|
|
57
84
|
type SyncOptions = SharedOptions & { agent: string; metadata?: boolean; closingIssue?: boolean; dryRun?: boolean; primaryResult: PullRequestPrimaryResult };
|
|
58
|
-
type ResolveExternalOptions = SharedOptions & { agent: string; pr?: number; dryRun?: boolean };
|
|
59
|
-
type SkipFactOptions = { cwd?: string; agent: string; dryRun?: boolean };
|
|
85
|
+
type ResolveExternalOptions = SharedOptions & { agent: string; pr?: string | number; dryRun?: boolean };
|
|
86
|
+
type SkipFactOptions = { cwd?: string; agent: string; dryRun?: boolean; runtimeVersion?: string };
|
|
60
87
|
type ExternalPullRequestSelection =
|
|
61
88
|
| { status: 'normal'; candidates: PullRequestSnapshot[]; eligible: [] }
|
|
62
89
|
| { status: 'selected'; source: 'unique' | 'explicit'; selected: PullRequestSnapshot; candidates: PullRequestSnapshot[]; eligible: PullRequestSnapshot[] }
|
|
@@ -222,9 +249,17 @@ function repositoryKey(repository: string): string {
|
|
|
222
249
|
return repository.trim().toLowerCase();
|
|
223
250
|
}
|
|
224
251
|
|
|
252
|
+
function isUsableResourceIdentity(identity: ResourceIdentity): boolean {
|
|
253
|
+
return isResourceIdentity(identity);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function identityLabel(identity: ResourceIdentity): string {
|
|
257
|
+
return identity.kind === 'number' ? `PR #${identity.value}` : `PR ${identity.kind}:${identity.value}`;
|
|
258
|
+
}
|
|
259
|
+
|
|
225
260
|
function identitySignature(pullRequest: PullRequestSnapshot): string {
|
|
226
261
|
return [
|
|
227
|
-
pullRequest.repository, pullRequest
|
|
262
|
+
pullRequest.repository, JSON.stringify(pullRequestIdentity(pullRequest)), pullRequest.nodeId, pullRequest.url,
|
|
228
263
|
pullRequest.head.repository, pullRequest.head.ref, pullRequest.head.sha,
|
|
229
264
|
pullRequest.base.repository, pullRequest.base.ref, pullRequest.base.sha,
|
|
230
265
|
pullRequest.mergedAt || '', pullRequest.mergeCommitSha || ''
|
|
@@ -237,34 +272,38 @@ function hasCompleteExternalIdentity(pullRequest: PullRequestSnapshot): boolean
|
|
|
237
272
|
pullRequest.head.repository, pullRequest.head.ref, pullRequest.head.sha,
|
|
238
273
|
pullRequest.base.repository, pullRequest.base.ref, pullRequest.base.sha
|
|
239
274
|
];
|
|
240
|
-
return
|
|
241
|
-
required.every((value) => Boolean(value?.trim())) &&
|
|
275
|
+
return isUsableResourceIdentity(pullRequestIdentity(pullRequest)) && required.every((value) => Boolean(value?.trim())) &&
|
|
242
276
|
(pullRequest.state === 'open' || Boolean(pullRequest.mergedAt) === Boolean(pullRequest.mergeCommitSha));
|
|
243
277
|
}
|
|
244
278
|
|
|
245
279
|
function selectExternalPullRequest(
|
|
246
280
|
candidates: PullRequestSnapshot[],
|
|
247
281
|
repository: string,
|
|
248
|
-
|
|
249
|
-
|
|
282
|
+
existingPrIdentity: ResourceIdentity | number | null,
|
|
283
|
+
explicitPrIdentity: ResourceIdentity | number | null
|
|
250
284
|
): ExternalPullRequestSelection {
|
|
285
|
+
const existingIdentity = typeof existingPrIdentity === 'number'
|
|
286
|
+
? resourceIdentity(existingPrIdentity) : existingPrIdentity;
|
|
287
|
+
const explicitIdentity = typeof explicitPrIdentity === 'number'
|
|
288
|
+
? resourceIdentity(explicitPrIdentity) : explicitPrIdentity;
|
|
251
289
|
const invalid = candidates.find((candidate) => !hasCompleteExternalIdentity(candidate));
|
|
252
290
|
if (invalid) return {
|
|
253
291
|
status: 'failed', code: 'PR_IDENTITY_INVALID',
|
|
254
292
|
message: `Closing PR #${invalid.number} lacks required identity`, candidates, eligible: []
|
|
255
293
|
};
|
|
256
|
-
const
|
|
294
|
+
const byIdentity = new Map<string, string>();
|
|
257
295
|
for (const candidate of candidates) {
|
|
258
296
|
const signature = identitySignature(candidate);
|
|
259
|
-
const
|
|
297
|
+
const identityKey = JSON.stringify(pullRequestIdentity(candidate));
|
|
298
|
+
const prior = byIdentity.get(identityKey);
|
|
260
299
|
if (prior && prior !== signature) return {
|
|
261
300
|
status: 'failed', code: 'PR_IDENTITY_INVALID',
|
|
262
301
|
message: `Closing PR #${candidate.number} has conflicting identities`, candidates, eligible: []
|
|
263
302
|
};
|
|
264
|
-
|
|
303
|
+
byIdentity.set(identityKey, signature);
|
|
265
304
|
}
|
|
266
305
|
const unique = candidates.filter((candidate, index) =>
|
|
267
|
-
candidates.findIndex((other) => other
|
|
306
|
+
candidates.findIndex((other) => resourceIdentityEquals(pullRequestIdentity(other), pullRequestIdentity(candidate))) === index
|
|
268
307
|
);
|
|
269
308
|
const wantedRepository = repositoryKey(repository);
|
|
270
309
|
const eligible = unique.filter((candidate) =>
|
|
@@ -273,19 +312,19 @@ function selectExternalPullRequest(
|
|
|
273
312
|
candidate.state === 'closed' &&
|
|
274
313
|
Boolean(candidate.mergedAt && candidate.mergeCommitSha)
|
|
275
314
|
);
|
|
276
|
-
if (
|
|
277
|
-
const selected = unique.find((candidate) => candidate
|
|
278
|
-
if (!selected) return { status: 'failed', code: 'PR_NOT_FOUND', message: `PR
|
|
279
|
-
if (!eligible.some((candidate) => candidate
|
|
280
|
-
return { status: 'failed', code: 'PR_IDENTITY_INVALID', message: `PR
|
|
315
|
+
if (explicitIdentity !== null) {
|
|
316
|
+
const selected = unique.find((candidate) => resourceIdentityEquals(pullRequestIdentity(candidate), explicitIdentity));
|
|
317
|
+
if (!selected) return { status: 'failed', code: 'PR_NOT_FOUND', message: `PR ${identityLabel(explicitIdentity)} is not a closing PR`, candidates, eligible };
|
|
318
|
+
if (!eligible.some((candidate) => resourceIdentityEquals(pullRequestIdentity(candidate), explicitIdentity))) {
|
|
319
|
+
return { status: 'failed', code: 'PR_IDENTITY_INVALID', message: `PR ${identityLabel(explicitIdentity)} is not an eligible merged PR`, candidates, eligible };
|
|
281
320
|
}
|
|
282
|
-
if (
|
|
283
|
-
return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to
|
|
321
|
+
if (existingIdentity !== null && !resourceIdentityEquals(existingIdentity, explicitIdentity)) {
|
|
322
|
+
return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to ${identityLabel(existingIdentity)}`, candidates, eligible };
|
|
284
323
|
}
|
|
285
324
|
return { status: 'selected', source: 'explicit', selected, candidates, eligible };
|
|
286
325
|
}
|
|
287
|
-
if (
|
|
288
|
-
return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to
|
|
326
|
+
if (existingIdentity !== null && eligible.length === 1 && !resourceIdentityEquals(existingIdentity, pullRequestIdentity(eligible[0]!))) {
|
|
327
|
+
return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to ${identityLabel(existingIdentity)}`, candidates, eligible };
|
|
289
328
|
}
|
|
290
329
|
if (eligible.length === 0) return { status: 'normal', candidates, eligible: [] };
|
|
291
330
|
if (eligible.length > 1) return {
|
|
@@ -364,64 +403,8 @@ function selectPullRequest(remotes: RemotePullRequest[], repository: string, hea
|
|
|
364
403
|
return { status: matches.length === 0 ? 'missing' : 'ambiguous', pullRequest: null };
|
|
365
404
|
}
|
|
366
405
|
|
|
367
|
-
function repositoryHead(cwd: string): string {
|
|
368
|
-
return execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
369
|
-
cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']
|
|
370
|
-
}).trim();
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function remoteBranchHead(cwd: string, repository: string, head: string):
|
|
374
|
-
| { ok: true; value: string }
|
|
375
|
-
| { ok: false; error: { code: string; message: string; retryable: boolean } } {
|
|
376
|
-
const wanted = expectedHead(repository, head);
|
|
377
|
-
if (!wanted.ref || !/^[A-Za-z0-9._/-]+$/.test(wanted.ref)) return {
|
|
378
|
-
ok: false,
|
|
379
|
-
error: { code: 'PR_HEAD_INVALID', message: 'Pull request head ref is invalid', retryable: false }
|
|
380
|
-
};
|
|
381
|
-
const remotes = configuredGitRemotes(cwd);
|
|
382
|
-
const remote = remotes.find((item) => repositoryKey(item.repository) === repositoryKey(wanted.repository))
|
|
383
|
-
?? remotes.find((item) => item.name === 'origin');
|
|
384
|
-
if (!remote) return {
|
|
385
|
-
ok: false,
|
|
386
|
-
error: { code: 'PR_REMOTE_BRANCH_MISSING', message: `No configured remote can verify ${wanted.repository}:${wanted.ref}`, retryable: false }
|
|
387
|
-
};
|
|
388
|
-
try {
|
|
389
|
-
const output = execFileSync('git', ['ls-remote', '--refs', remote.name, `refs/heads/${wanted.ref}`], {
|
|
390
|
-
cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']
|
|
391
|
-
}).trim();
|
|
392
|
-
const match = output.split(/\r?\n/).map((line) => line.trim()).find((line) => line.endsWith(`refs/heads/${wanted.ref}`));
|
|
393
|
-
const sha = match?.split(/\s+/)[0] || null;
|
|
394
|
-
if (!sha || !/^[a-f0-9]{40}$/i.test(sha)) return {
|
|
395
|
-
ok: false,
|
|
396
|
-
error: { code: 'PR_REMOTE_BRANCH_MISSING', message: `Remote branch ${wanted.repository}:${wanted.ref} does not exist`, retryable: false }
|
|
397
|
-
};
|
|
398
|
-
return { ok: true, value: sha };
|
|
399
|
-
} catch (error) {
|
|
400
|
-
return {
|
|
401
|
-
ok: false,
|
|
402
|
-
error: { code: 'PR_REMOTE_BRANCH_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: true }
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
function verifyCreateHead(base: ReturnType<typeof resolvedContext> & { ok: true }, head: string) {
|
|
408
|
-
let localHead: string;
|
|
409
|
-
try {
|
|
410
|
-
localHead = repositoryHead(base.resolved.repoRoot);
|
|
411
|
-
} catch (error) {
|
|
412
|
-
return { ok: false as const, error: { code: 'PR_LOCAL_HEAD_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: false } };
|
|
413
|
-
}
|
|
414
|
-
const remote = remoteBranchHead(base.resolved.repoRoot, base.context.platform.repository!, head);
|
|
415
|
-
if (!remote.ok) return remote;
|
|
416
|
-
if (remote.value !== localHead) return {
|
|
417
|
-
ok: false as const,
|
|
418
|
-
error: { code: 'PR_REMOTE_HEAD_MISMATCH', message: `Remote head ${remote.value} does not match local HEAD ${localHead}`, retryable: false }
|
|
419
|
-
};
|
|
420
|
-
return { ok: true as const, value: localHead };
|
|
421
|
-
}
|
|
422
|
-
|
|
423
406
|
function validateBoundPullRequest(
|
|
424
|
-
base: ReturnType<typeof resolvedContext
|
|
407
|
+
base: Awaited<ReturnType<typeof resolvedContext>> & { ok: true },
|
|
425
408
|
pullRequest: PullRequestSnapshot | null,
|
|
426
409
|
head: string,
|
|
427
410
|
baseRef: string,
|
|
@@ -464,7 +447,7 @@ function resolveTaskDeliveryTarget(repoRoot: string, frontmatter: Record<string,
|
|
|
464
447
|
: { ok: false as const, error: { code: target.code, message: target.message, retryable: false } };
|
|
465
448
|
}
|
|
466
449
|
|
|
467
|
-
function taskDeliveryTarget(base: ReturnType<typeof resolvedContext
|
|
450
|
+
function taskDeliveryTarget(base: Awaited<ReturnType<typeof resolvedContext>> & { ok: true }) {
|
|
468
451
|
return resolveTaskDeliveryTarget(base.resolved.repoRoot, base.frontmatter);
|
|
469
452
|
}
|
|
470
453
|
|
|
@@ -496,7 +479,7 @@ function validateIdentityAgainstTarget(
|
|
|
496
479
|
}
|
|
497
480
|
|
|
498
481
|
function validateWriterIdentity(
|
|
499
|
-
base: ReturnType<typeof resolvedContext
|
|
482
|
+
base: Awaited<ReturnType<typeof resolvedContext>> & { ok: true },
|
|
500
483
|
pullRequest: PullRequestSnapshot | null,
|
|
501
484
|
options: BindingIdentityOptions = {}
|
|
502
485
|
): { ok: true } | { ok: false; error: { code: string; message: string; retryable: boolean } } {
|
|
@@ -529,29 +512,103 @@ function validateExternalMergedEvidence(
|
|
|
529
512
|
return { ok: true };
|
|
530
513
|
}
|
|
531
514
|
|
|
532
|
-
function resolvedContext(taskRef: string, options: InspectionOptions) {
|
|
515
|
+
async function resolvedContext(taskRef: string, options: InspectionOptions) {
|
|
533
516
|
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
534
517
|
if (!resolved.ok) return { ok: false as const, output: result('failed', resolved.taskId, null, null, {
|
|
535
518
|
error: { code: resolved.code, message: resolved.message, retryable: false }
|
|
536
519
|
}) };
|
|
537
520
|
const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
538
521
|
const frontmatter = parseTypedTaskFrontmatter(content);
|
|
539
|
-
const factRead = readPrDeliveryFact(frontmatter);
|
|
522
|
+
const factRead = readPrDeliveryFact(frontmatter, options.runtimeVersion);
|
|
540
523
|
if (factRead.status === 'invalid') return { ok: false as const, output: result('failed', resolved.taskId, null, null, {
|
|
541
|
-
error: { code:
|
|
524
|
+
error: { code: factRead.error.code, message: factRead.error.message, retryable: false }
|
|
542
525
|
}) };
|
|
543
526
|
const fact = factRead.status === 'valid' ? factRead.fact : null;
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
const
|
|
547
|
-
const
|
|
548
|
-
const
|
|
549
|
-
const
|
|
527
|
+
let issueIdentity: ResourceIdentity | null;
|
|
528
|
+
try { issueIdentity = taskIssueIdentity(frontmatter, undefined, options.runtimeVersion); }
|
|
529
|
+
catch (error) { return { ok: false as const, output: result('failed', resolved.taskId, null, null, { error: { ...taskIssueIdentityError(error), retryable: false } }) }; }
|
|
530
|
+
const issueNumber = resourceIdentityNumber(issueIdentity);
|
|
531
|
+
const prIdentity = fact?.state === 'bound' ? fact.identity.resource : null;
|
|
532
|
+
const prNumber = resourceIdentityNumber(prIdentity);
|
|
533
|
+
const client = options.client;
|
|
534
|
+
const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client });
|
|
535
|
+
const context = loaded.ok ? loaded.value.context : loaded.context;
|
|
550
536
|
const usable = (context.status === 'no-op' || context.status === 'degraded') && context.platform.repository;
|
|
551
|
-
if (!usable) return { ok: false as const, output: result(context.status, resolved.taskId, issueNumber, prNumber, {
|
|
537
|
+
if (!usable || !loaded.ok) return { ok: false as const, output: result(context.status, resolved.taskId, issueNumber, prNumber, {
|
|
552
538
|
platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
|
|
553
539
|
}) };
|
|
554
|
-
return { ok: true as const, resolved, content, frontmatter, fact, issueNumber, prNumber, client, context };
|
|
540
|
+
return { ok: true as const, resolved, content, frontmatter, fact, issueIdentity, issueNumber, prIdentity, prNumber, client, context, provider: loaded.value.provider, providerType: loaded.value.providerType, loadedContext: loaded.value };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function normalizeProviderPullRequest(
|
|
544
|
+
remote: ProviderChangeRequestSnapshot,
|
|
545
|
+
repository: string,
|
|
546
|
+
fallbackNumber: number,
|
|
547
|
+
fallback?: PullRequestSnapshot | null
|
|
548
|
+
): PullRequestSnapshot {
|
|
549
|
+
const head = remote.head || fallback?.head || { repository, ref: '', sha: remote.headSha || '' };
|
|
550
|
+
const base = remote.base || fallback?.base || { repository, ref: '', sha: remote.baseSha || '' };
|
|
551
|
+
const identity = remote.identity || fallback?.identity || (remote.number ? { kind: 'number' as const, value: remote.number } : { kind: 'id' as const, value: remote.id });
|
|
552
|
+
const snapshot: PullRequestSnapshot = {
|
|
553
|
+
repository,
|
|
554
|
+
number: remote.number ?? resourceIdentityNumber(identity) ?? fallbackNumber,
|
|
555
|
+
nodeId: remote.id,
|
|
556
|
+
url: remote.displayUrl || fallback?.url || '',
|
|
557
|
+
state: remote.state === 'closed' ? 'closed' : 'open',
|
|
558
|
+
title: remote.title,
|
|
559
|
+
body: remote.body,
|
|
560
|
+
draft: remote.draft ?? fallback?.draft ?? false,
|
|
561
|
+
head: { ...head, sha: remote.headSha || head.sha },
|
|
562
|
+
base: { ...base, sha: remote.baseSha || base.sha },
|
|
563
|
+
mergedAt: remote.mergedAt ?? fallback?.mergedAt ?? null,
|
|
564
|
+
mergeCommitSha: remote.mergeCommitSha ?? fallback?.mergeCommitSha ?? null,
|
|
565
|
+
labels: [...(remote.labels || fallback?.labels || [])].sort(),
|
|
566
|
+
assignees: [...(remote.assignees || fallback?.assignees || [])].sort(),
|
|
567
|
+
milestone: remote.milestone ?? fallback?.milestone ?? null,
|
|
568
|
+
mergeability: remote.mergeability || fallback?.mergeability || { state: 'unknown', detail: null }
|
|
569
|
+
};
|
|
570
|
+
Object.defineProperty(snapshot, 'identity', { value: identity, enumerable: false, configurable: true });
|
|
571
|
+
return snapshot;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function pullRequestIdentity(pullRequest: PullRequestSnapshot): ResourceIdentity {
|
|
575
|
+
return pullRequest.identity || { kind: 'number', value: pullRequest.number };
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function providerPullRequestError(
|
|
579
|
+
base: Awaited<ReturnType<typeof resolvedContext>> & { ok: true },
|
|
580
|
+
error: { code: string; message: string; retryable: boolean },
|
|
581
|
+
prNumber: number | null
|
|
582
|
+
): PullRequestResult {
|
|
583
|
+
return result(error.code.startsWith('IN_LABEL_SYNC') ? 'blocked' : providerStatus(error), base.resolved.taskId, base.issueNumber, prNumber, {
|
|
584
|
+
changed: error.code.startsWith('IN_LABEL_SYNC'),
|
|
585
|
+
platform: base.context.platform,
|
|
586
|
+
capabilities: base.context.capabilities,
|
|
587
|
+
resource: { kind: 'pull-request', number: prNumber },
|
|
588
|
+
error: providerError(error, 'PLATFORM_PROVIDER_OPERATION_FAILED')
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
async function inspectExternalPullRequest(
|
|
593
|
+
base: Awaited<ReturnType<typeof resolvedContext>> & { ok: true },
|
|
594
|
+
prIdentity: ResourceIdentity
|
|
595
|
+
): Promise<PullRequestResult> {
|
|
596
|
+
const prNumber = resourceIdentityNumber(prIdentity);
|
|
597
|
+
const fallback = base.fact?.state === 'bound' && resourceIdentityEquals(base.fact.identity.resource, prIdentity)
|
|
598
|
+
? base.fact.identity as unknown as PullRequestSnapshot
|
|
599
|
+
: null;
|
|
600
|
+
const inspected = base.provider.changeRequests?.inspect
|
|
601
|
+
? await base.provider.changeRequests.inspect({ context: providerOperationContext(base.loadedContext), target: prIdentity })
|
|
602
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
|
|
603
|
+
if (!inspected.ok) return providerPullRequestError(base, inspected.error, prNumber);
|
|
604
|
+
const pullRequest = normalizeProviderPullRequest(inspected.value, base.context.platform.repository!, prNumber || 0, fallback);
|
|
605
|
+
return result('no-op', base.resolved.taskId, base.issueNumber, prNumber, {
|
|
606
|
+
platform: base.context.platform,
|
|
607
|
+
capabilities: base.context.capabilities,
|
|
608
|
+
resource: { kind: 'pull-request', number: prNumber, identity: prIdentity },
|
|
609
|
+
pullRequest,
|
|
610
|
+
error: null
|
|
611
|
+
});
|
|
555
612
|
}
|
|
556
613
|
|
|
557
614
|
function inspectGitHubPullRequest(client: GitHubClient, repository: string, number: number, cwd: string) {
|
|
@@ -563,203 +620,348 @@ function inspectGitHubPullRequest(client: GitHubClient, repository: string, numb
|
|
|
563
620
|
: { ok: false as const, error: { code: 'PR_IDENTITY_INVALID', message: 'Remote resource is not a valid pull request', retryable: false } };
|
|
564
621
|
}
|
|
565
622
|
|
|
566
|
-
function
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
623
|
+
async function inspectPlatformPullRequest(taskRef: string, options: InspectionOptions = {}): Promise<PullRequestResult> {
|
|
624
|
+
const base = await resolvedContext(taskRef, options);
|
|
625
|
+
if (!base.ok) return base.output;
|
|
626
|
+
if (!base.fact) return result('no-op', base.resolved.taskId, base.issueNumber, null, {
|
|
627
|
+
error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
|
|
628
|
+
});
|
|
629
|
+
if (!base.prIdentity) return result('no-op', base.resolved.taskId, base.issueNumber, null, {
|
|
630
|
+
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
631
|
+
error: { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false }
|
|
632
|
+
});
|
|
633
|
+
return inspectExternalPullRequest(base, base.prIdentity);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
async function inspectPlatformPullRequestByNumber(prNumber: string | number, options: InspectionOptions = {}): Promise<PullRequestResult> {
|
|
637
|
+
const loaded = await resolvePlatformProviderContext({ cwd: options.cwd || process.cwd(), client: options.client });
|
|
638
|
+
const context = loaded.ok ? loaded.value.context : loaded.context;
|
|
639
|
+
const usable = (context.status === 'no-op' || context.status === 'degraded') && context.platform.repository;
|
|
640
|
+
if (!usable) {
|
|
641
|
+
return result(context.status, null, null, null, {
|
|
642
|
+
platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
let identity: ResourceIdentity;
|
|
646
|
+
try { identity = loaded.ok ? providerResourceToken(loaded.value.provider, 'pull-request', String(prNumber)) : resourceIdentity(prNumber); }
|
|
647
|
+
catch (error) {
|
|
648
|
+
return result('failed', null, null, null, {
|
|
649
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
650
|
+
error: { code: 'PLATFORM_IDENTITY_TOKEN_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
580
651
|
});
|
|
581
|
-
} catch {
|
|
582
|
-
return [];
|
|
583
652
|
}
|
|
653
|
+
if (loaded.ok) {
|
|
654
|
+
const inspected = loaded.value.provider.changeRequests?.inspect
|
|
655
|
+
? await loaded.value.provider.changeRequests.inspect({ context: providerOperationContext(loaded.value), target: identity })
|
|
656
|
+
: unsupportedProviderOperation(loaded.value.provider, 'changeRequests.inspect');
|
|
657
|
+
const number = resourceIdentityNumber(identity);
|
|
658
|
+
if (!inspected.ok) return result(providerStatus(inspected.error), null, null, number, {
|
|
659
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
660
|
+
resource: { kind: 'pull-request', number, identity }, error: providerError(inspected.error, 'PLATFORM_PROVIDER_OPERATION_FAILED')
|
|
661
|
+
});
|
|
662
|
+
return result('no-op', null, null, number, {
|
|
663
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
664
|
+
resource: { kind: 'pull-request', number, identity },
|
|
665
|
+
pullRequest: normalizeProviderPullRequest(inspected.value, context.platform.repository!, number || 0), error: null
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
return result('failed', null, null, null, {
|
|
669
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
670
|
+
resource: { kind: 'pull-request', number: null },
|
|
671
|
+
error: { code: 'PLATFORM_PROVIDER_LOAD_FAILED', message: 'Selected provider failed to load', retryable: false }
|
|
672
|
+
});
|
|
584
673
|
}
|
|
585
674
|
|
|
586
|
-
|
|
587
|
-
const match = remote.trim().match(
|
|
588
|
-
/^(https?:\/\/github\.com\/|ssh:\/\/(?:git@)?github\.com\/|git@github\.com:)[^/\s]+\/[^/\s]+(?:\.git)?$/i
|
|
589
|
-
);
|
|
590
|
-
return match ? `${match[1]}${repository}.git` : null;
|
|
591
|
-
}
|
|
675
|
+
const CLOSING_ISSUES_QUERY = 'query($owner:String!,$name:String!,$number:Int!,$cursor:String){repository(owner:$owner,name:$name){pullRequest(number:$number){closingIssuesReferences(first:100,after:$cursor){nodes{number} pageInfo{hasNextPage endCursor}}}}}';
|
|
592
676
|
|
|
593
|
-
function
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
}) {
|
|
602
|
-
if (
|
|
603
|
-
pullRequest.repository !== repository ||
|
|
604
|
-
pullRequest.base.repository !== repository ||
|
|
605
|
-
pullRequest.number <= 0
|
|
606
|
-
) {
|
|
677
|
+
function readInLabelMapping(repoRoot: string): { ok: true; value: Record<string, unknown> } | { ok: false; error: { code: string; message: string; retryable: boolean } } {
|
|
678
|
+
try {
|
|
679
|
+
const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8')) as {
|
|
680
|
+
labels?: { in?: unknown };
|
|
681
|
+
};
|
|
682
|
+
const mapping = validateInLabelMapping(config.labels?.in);
|
|
683
|
+
return mapping.ok ? { ok: true, value: mapping.value } : mapping;
|
|
684
|
+
} catch (error) {
|
|
607
685
|
return {
|
|
608
|
-
ok: false
|
|
609
|
-
error: {
|
|
610
|
-
code: 'PR_MERGE_EVIDENCE_SOURCE_UNAVAILABLE',
|
|
611
|
-
message: 'Pull request repository identity is inconsistent',
|
|
612
|
-
retryable: false
|
|
613
|
-
}
|
|
686
|
+
ok: false,
|
|
687
|
+
error: { code: 'IN_LABEL_SYNC_CONFIG_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
614
688
|
};
|
|
615
689
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function taskInLabelTarget(
|
|
693
|
+
repoRoot: string,
|
|
694
|
+
frontmatter: Record<string, unknown>,
|
|
695
|
+
currentLabels: readonly string[],
|
|
696
|
+
repositoryLabels: Set<string>
|
|
697
|
+
) {
|
|
698
|
+
const baseRef = typeof frontmatter.delivery_base_ref === 'string' ? frontmatter.delivery_base_ref.trim() : '';
|
|
699
|
+
if (!baseRef) return {
|
|
700
|
+
ok: false as const,
|
|
701
|
+
error: { code: 'IN_LABEL_SYNC_BASE_MISSING', message: 'Task has no delivery_base_ref for in-label evidence', retryable: false }
|
|
627
702
|
};
|
|
628
|
-
|
|
703
|
+
let changedFiles: string[];
|
|
704
|
+
try {
|
|
705
|
+
changedFiles = execFileSync('git', ['diff', `${baseRef}...HEAD`, '--name-only'], {
|
|
706
|
+
cwd: repoRoot, encoding: 'utf8'
|
|
707
|
+
}).trim().split(/\r?\n/).filter(Boolean);
|
|
708
|
+
} catch (error) {
|
|
629
709
|
return {
|
|
630
710
|
ok: false as const,
|
|
631
|
-
error: {
|
|
632
|
-
code: 'PR_MERGE_EVIDENCE_SOURCE_UNAVAILABLE',
|
|
633
|
-
message: 'Pull request Git refs are invalid',
|
|
634
|
-
retryable: false
|
|
635
|
-
}
|
|
711
|
+
error: { code: 'IN_LABEL_SYNC_EVIDENCE_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
636
712
|
};
|
|
637
713
|
}
|
|
714
|
+
const mapping = readInLabelMapping(repoRoot);
|
|
715
|
+
if (!mapping.ok) return mapping;
|
|
716
|
+
const planned = planInLabelUpdate({ changedFiles, currentLabels, mapping: mapping.value, repositoryLabels });
|
|
717
|
+
if (planned.error) return { ok: false as const, error: planned.error };
|
|
718
|
+
return { ok: true as const, value: planned.target };
|
|
719
|
+
}
|
|
638
720
|
|
|
639
|
-
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
721
|
+
function inspectClosingIssueNumbers(client: GitHubClient, repository: string, pullRequest: number, cwd: string) {
|
|
722
|
+
const [owner, name] = repository.split('/');
|
|
723
|
+
if (!owner || !name) return {
|
|
724
|
+
ok: false as const,
|
|
725
|
+
error: { code: 'IN_LABEL_SYNC_REPOSITORY_INVALID', message: 'Repository identity is invalid', retryable: false }
|
|
726
|
+
};
|
|
727
|
+
let cursor: string | null = null;
|
|
728
|
+
const numbers: number[] = [];
|
|
729
|
+
for (;;) {
|
|
730
|
+
const args = ['api', 'graphql', '-f', `query=${CLOSING_ISSUES_QUERY}`, '-F', `owner=${owner}`, '-F', `name=${name}`, '-F', `number=${pullRequest}`];
|
|
731
|
+
if (cursor) args.push('-F', `cursor=${cursor}`);
|
|
732
|
+
const response = client.json<unknown>(args, { cwd });
|
|
733
|
+
if (!response.ok) return response;
|
|
734
|
+
const connection = (response.value as {
|
|
735
|
+
data?: { repository?: { pullRequest?: { closingIssuesReferences?: {
|
|
736
|
+
nodes?: unknown[]; pageInfo?: { hasNextPage?: boolean; endCursor?: string | null };
|
|
737
|
+
} } } };
|
|
738
|
+
})?.data?.repository?.pullRequest?.closingIssuesReferences;
|
|
739
|
+
if (!connection || !Array.isArray(connection.nodes) || !connection.pageInfo) return {
|
|
648
740
|
ok: false as const,
|
|
649
|
-
error: {
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
741
|
+
error: { code: 'IN_LABEL_SYNC_ASSOCIATION_INVALID', message: 'Closing Issue response is incomplete', retryable: false }
|
|
742
|
+
};
|
|
743
|
+
for (const node of connection.nodes) {
|
|
744
|
+
const issue = Number((node as { number?: unknown })?.number);
|
|
745
|
+
if (!Number.isSafeInteger(issue) || issue <= 0) return {
|
|
746
|
+
ok: false as const,
|
|
747
|
+
error: { code: 'IN_LABEL_SYNC_ASSOCIATION_INVALID', message: 'Closing Issue identity is invalid', retryable: false }
|
|
748
|
+
};
|
|
749
|
+
numbers.push(issue);
|
|
750
|
+
}
|
|
751
|
+
if (!connection.pageInfo.hasNextPage) return { ok: true as const, value: [...new Set(numbers)] };
|
|
752
|
+
if (!connection.pageInfo.endCursor || connection.pageInfo.endCursor === cursor) return {
|
|
753
|
+
ok: false as const,
|
|
754
|
+
error: { code: 'IN_LABEL_SYNC_ASSOCIATION_INVALID', message: 'Closing Issue pagination cursor is invalid', retryable: false }
|
|
654
755
|
};
|
|
756
|
+
cursor = connection.pageInfo.endCursor;
|
|
655
757
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function inspectRepositoryLabels(client: GitHubClient, repository: string, cwd: string) {
|
|
761
|
+
const response = client.json<unknown>(['api', '--paginate', '--slurp', `repos/${repository}/labels?per_page=100`], { cwd });
|
|
762
|
+
if (!response.ok) return response;
|
|
763
|
+
const labels = validateRepositoryLabelPayload(response.value);
|
|
764
|
+
return labels.ok ? { ok: true as const, value: new Set(labels.value) } : labels;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function inspectPullRequestFiles(client: GitHubClient, repository: string, pullRequest: number, cwd: string) {
|
|
768
|
+
const response = client.json<unknown>(['api', '--paginate', '--slurp', `repos/${repository}/pulls/${pullRequest}/files?per_page=100`], { cwd });
|
|
769
|
+
if (!response.ok) return response;
|
|
770
|
+
const files = extractPullRequestFileNames(response.value);
|
|
771
|
+
return files ? { ok: true as const, value: files } : {
|
|
772
|
+
ok: false as const,
|
|
773
|
+
error: { code: 'IN_LABEL_SYNC_FILES_INVALID', message: 'Pull request files response is incomplete', retryable: false }
|
|
659
774
|
};
|
|
660
775
|
}
|
|
661
776
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
777
|
+
function inspectTaskInLabelTarget(
|
|
778
|
+
repoRoot: string,
|
|
779
|
+
frontmatter: Record<string, unknown>,
|
|
780
|
+
pullRequest: PullRequestSnapshot,
|
|
781
|
+
repository: string,
|
|
782
|
+
client: GitHubClient
|
|
783
|
+
) {
|
|
784
|
+
const baseRef = typeof frontmatter.delivery_base_ref === 'string' ? frontmatter.delivery_base_ref.trim() : '';
|
|
785
|
+
if (!baseRef) return {
|
|
786
|
+
ok: false as const,
|
|
787
|
+
error: { code: 'IN_LABEL_SYNC_BASE_MISSING', message: 'Task has no delivery_base_ref for in-label evidence', retryable: false }
|
|
788
|
+
};
|
|
789
|
+
let changedFiles: string[];
|
|
790
|
+
try {
|
|
791
|
+
changedFiles = execFileSync('git', ['diff', `${baseRef}...HEAD`, '--name-only'], {
|
|
792
|
+
cwd: repoRoot, encoding: 'utf8'
|
|
793
|
+
}).trim().split(/\r?\n/).filter(Boolean);
|
|
794
|
+
} catch (error) {
|
|
795
|
+
return {
|
|
796
|
+
ok: false as const,
|
|
797
|
+
error: { code: 'IN_LABEL_SYNC_EVIDENCE_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
798
|
+
};
|
|
673
799
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
if (
|
|
682
|
-
|
|
683
|
-
return selected.status === 'resolved'
|
|
684
|
-
? { ok: true as const, value: selected.pullRequest }
|
|
685
|
-
: { ok: false as const, error: {
|
|
686
|
-
code: selected.status === 'ambiguous' ? 'PR_IDENTITY_AMBIGUOUS'
|
|
687
|
-
: selected.status === 'head-mismatch' ? 'PR_HEAD_SHA_MISMATCH' : 'PR_NOT_FOUND',
|
|
688
|
-
message: selected.status === 'ambiguous' ? 'Multiple pull requests match the exact head/base identity'
|
|
689
|
-
: selected.status === 'head-mismatch' ? 'A pull request matches head/base but not the expected head SHA'
|
|
690
|
-
: 'No pull request matches the exact head/base identity',
|
|
691
|
-
retryable: false
|
|
692
|
-
} };
|
|
800
|
+
const mapping = readInLabelMapping(repoRoot);
|
|
801
|
+
if (!mapping.ok) return mapping;
|
|
802
|
+
const labels = inspectRepositoryLabels(client, repository, repoRoot);
|
|
803
|
+
if (!labels.ok) return labels;
|
|
804
|
+
const planned = planInLabelUpdate({
|
|
805
|
+
changedFiles, currentLabels: pullRequest.labels, mapping: mapping.value, repositoryLabels: labels.value
|
|
806
|
+
});
|
|
807
|
+
if (planned.error) return { ok: false as const, error: planned.error };
|
|
808
|
+
return { ok: true as const, value: { changedFiles, target: planned.target } };
|
|
693
809
|
}
|
|
694
810
|
|
|
695
|
-
function
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
811
|
+
function inLabelResource(
|
|
812
|
+
kind: 'issue' | 'pull-request',
|
|
813
|
+
number: number,
|
|
814
|
+
before: string[],
|
|
815
|
+
expected: string[],
|
|
816
|
+
after: string[] | null,
|
|
817
|
+
effect: 'no-op' | 'applied' | 'unknown'
|
|
818
|
+
) {
|
|
819
|
+
return { kind, number, before: [...before].sort(), expected: [...expected].sort(), after: after ? [...after].sort() : null, effect };
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
async function syncPlatformPullRequestInLabels(prNumber: number, options: SharedOptions & { dryRun?: boolean } = {}): Promise<PullRequestResult> {
|
|
823
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
824
|
+
const client = options.client || createGitHubClient();
|
|
825
|
+
const context = await resolvePlatformContext({ cwd, client });
|
|
826
|
+
const usable = (context.status === 'no-op' || context.status === 'degraded') && context.platform.repository;
|
|
827
|
+
if (!usable) return result(context.status, null, null, prNumber, {
|
|
828
|
+
platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
|
|
700
829
|
});
|
|
701
|
-
if (!
|
|
702
|
-
platform:
|
|
703
|
-
error: { code: '
|
|
830
|
+
if (!Number.isSafeInteger(prNumber) || prNumber <= 0) return result('failed', null, null, prNumber, {
|
|
831
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
832
|
+
error: { code: 'PR_NUMBER_INVALID', message: 'PR number must be positive', retryable: false }
|
|
704
833
|
});
|
|
705
|
-
const
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
number:
|
|
709
|
-
client: options.client || base.client
|
|
834
|
+
const repository = context.platform.repository!;
|
|
835
|
+
const fetched = inspectGitHubPullRequest(client, repository, prNumber, cwd);
|
|
836
|
+
if (!fetched.ok) return result(fetched.error.retryable ? 'blocked' : 'failed', null, null, prNumber, {
|
|
837
|
+
platform: context.platform, capabilities: context.capabilities, resource: { kind: 'pull-request', number: prNumber }, error: fetched.error
|
|
710
838
|
});
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
return result('
|
|
723
|
-
platform:
|
|
724
|
-
resource: { kind: 'pull-request', number:
|
|
839
|
+
const files = inspectPullRequestFiles(client, repository, prNumber, cwd);
|
|
840
|
+
if (!files.ok) return result(files.error.retryable ? 'blocked' : 'failed', null, null, prNumber, {
|
|
841
|
+
platform: context.platform, capabilities: context.capabilities, pullRequest: fetched.value,
|
|
842
|
+
resource: { kind: 'pull-request', number: prNumber }, error: files.error
|
|
843
|
+
});
|
|
844
|
+
const mapping = readInLabelMapping(cwd);
|
|
845
|
+
if (!mapping.ok) return result('failed', null, null, prNumber, {
|
|
846
|
+
platform: context.platform, capabilities: context.capabilities, pullRequest: fetched.value,
|
|
847
|
+
resource: { kind: 'pull-request', number: prNumber }, error: mapping.error
|
|
848
|
+
});
|
|
849
|
+
const repositoryLabels = inspectRepositoryLabels(client, repository, cwd);
|
|
850
|
+
if (!repositoryLabels.ok) return result(repositoryLabels.error.retryable ? 'blocked' : 'failed', null, null, prNumber, {
|
|
851
|
+
platform: context.platform, capabilities: context.capabilities, pullRequest: fetched.value,
|
|
852
|
+
resource: { kind: 'pull-request', number: prNumber }, error: repositoryLabels.error
|
|
853
|
+
});
|
|
854
|
+
const plannedTarget = planInLabelUpdate({
|
|
855
|
+
changedFiles: files.value, currentLabels: fetched.value.labels,
|
|
856
|
+
mapping: mapping.value, repositoryLabels: repositoryLabels.value
|
|
857
|
+
});
|
|
858
|
+
if (plannedTarget.error) return result('failed', null, null, prNumber, {
|
|
859
|
+
platform: context.platform, capabilities: context.capabilities, pullRequest: fetched.value,
|
|
860
|
+
resource: { kind: 'pull-request', number: prNumber }, error: plannedTarget.error
|
|
861
|
+
});
|
|
862
|
+
const target = plannedTarget.target;
|
|
863
|
+
const association = inspectClosingIssueNumbers(client, repository, prNumber, cwd);
|
|
864
|
+
if (!association.ok) return result(association.error.retryable ? 'blocked' : 'failed', null, null, prNumber, {
|
|
865
|
+
platform: context.platform, capabilities: context.capabilities, pullRequest: fetched.value,
|
|
866
|
+
resource: { kind: 'pull-request', number: prNumber }, evidence: { kind: 'pull-request-files', pullRequestFiles: files.value }, error: association.error
|
|
725
867
|
});
|
|
726
|
-
}
|
|
727
868
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
869
|
+
const issueNumber = association.value.length === 1 ? association.value[0]! : null;
|
|
870
|
+
let issue: IssueSnapshot | null = null;
|
|
871
|
+
if (issueNumber) {
|
|
872
|
+
const fetchedIssue = inspectGitHubIssue(client, repository, issueNumber, cwd);
|
|
873
|
+
if (!fetchedIssue.ok) return result(fetchedIssue.error.retryable ? 'blocked' : 'failed', null, issueNumber, prNumber, {
|
|
874
|
+
platform: context.platform, capabilities: context.capabilities, pullRequest: fetched.value,
|
|
875
|
+
resource: { kind: 'issue', number: issueNumber }, evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: association.value }, error: fetchedIssue.error
|
|
735
876
|
});
|
|
877
|
+
issue = fetchedIssue.value;
|
|
736
878
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
879
|
+
|
|
880
|
+
const resources = issue
|
|
881
|
+
? [
|
|
882
|
+
{ kind: 'issue' as const, number: issue.number, snapshot: issue },
|
|
883
|
+
{ kind: 'pull-request' as const, number: prNumber, snapshot: fetched.value }
|
|
884
|
+
]
|
|
885
|
+
: [{ kind: 'pull-request' as const, number: prNumber, snapshot: fetched.value }];
|
|
886
|
+
const plans = resources.map((resource) => ({
|
|
887
|
+
...resource,
|
|
888
|
+
plan: planInLabelUpdate({
|
|
889
|
+
changedFiles: files.value, currentLabels: resource.snapshot.labels,
|
|
890
|
+
mapping: mapping.value, repositoryLabels: repositoryLabels.value
|
|
891
|
+
})
|
|
892
|
+
}));
|
|
893
|
+
const operations: PlatformOperation[] = [
|
|
894
|
+
{ name: 'closing-issues', status: 'no-op', reasonCode: association.value.length === 1 ? null : association.value.length === 0 ? 'ISSUE_ASSOCIATION_NONE' : 'ISSUE_ASSOCIATION_AMBIGUOUS' },
|
|
895
|
+
...plans.map((item) => ({
|
|
896
|
+
name: `labels:in:${item.kind}`,
|
|
897
|
+
status: !context.capabilities.triage ? 'skipped' as const : item.plan.changed ? 'planned' as const : 'no-op' as const,
|
|
898
|
+
reasonCode: !context.capabilities.triage ? 'TRIAGE_REQUIRED' : null
|
|
899
|
+
}))
|
|
900
|
+
];
|
|
901
|
+
const willWrite = context.capabilities.triage && !options.dryRun;
|
|
902
|
+
const resourcesState = plans.map((item) => inLabelResource(
|
|
903
|
+
item.kind,
|
|
904
|
+
item.number,
|
|
905
|
+
item.snapshot.labels,
|
|
906
|
+
target,
|
|
907
|
+
item.plan.changed && willWrite ? null : item.snapshot.labels,
|
|
908
|
+
item.plan.changed && willWrite ? 'unknown' : 'no-op'
|
|
909
|
+
));
|
|
910
|
+
if (options.dryRun || !context.capabilities.triage) return result(
|
|
911
|
+
!context.capabilities.triage || association.value.length !== 1 ? 'degraded' : plans.some((item) => item.plan.changed) ? 'planned' : 'no-op',
|
|
912
|
+
null, issueNumber, prNumber, {
|
|
913
|
+
platform: context.platform, capabilities: context.capabilities, pullRequest: fetched.value,
|
|
914
|
+
resource: { kind: 'pull-request', number: prNumber }, operations, resources: resourcesState,
|
|
915
|
+
evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: association.value }, error: null
|
|
916
|
+
}
|
|
917
|
+
);
|
|
918
|
+
|
|
919
|
+
let currentPullRequest = fetched.value;
|
|
920
|
+
let successfulWrites = 0;
|
|
921
|
+
for (const item of plans) {
|
|
922
|
+
if (!item.plan.changed) continue;
|
|
923
|
+
const synced = syncLabelDelta(client, repository, item.number, cwd, item.snapshot.labels, item.plan.target);
|
|
924
|
+
if (synced.status === 'failed' || synced.status === 'blocked') {
|
|
925
|
+
const state = resourcesState.find((resource) => resource.kind === item.kind && resource.number === item.number)!;
|
|
926
|
+
state.effect = synced.status === 'blocked' ? 'unknown' : 'no-op';
|
|
927
|
+
state.after = synced.status === 'blocked' ? null : item.snapshot.labels;
|
|
928
|
+
const partial = successfulWrites > 0 || synced.status === 'blocked';
|
|
929
|
+
const error = partial && synced.error
|
|
930
|
+
? { ...synced.error, code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${synced.error.message}` }
|
|
931
|
+
: synced.error;
|
|
932
|
+
return result(partial ? 'blocked' : synced.status, null, issueNumber, prNumber, {
|
|
933
|
+
changed: successfulWrites > 0 || synced.changed, platform: context.platform, capabilities: context.capabilities,
|
|
934
|
+
pullRequest: currentPullRequest, resource: { kind: 'pull-request', number: prNumber }, operations, resources: resourcesState,
|
|
935
|
+
evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: association.value }, error
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
successfulWrites += synced.changed ? 1 : 0;
|
|
939
|
+
const reread = item.kind === 'issue'
|
|
940
|
+
? inspectGitHubIssue(client, repository, item.number, cwd)
|
|
941
|
+
: inspectGitHubPullRequest(client, repository, item.number, cwd);
|
|
942
|
+
if (!reread.ok) return result('blocked', null, issueNumber, prNumber, {
|
|
943
|
+
changed: true, platform: context.platform, capabilities: context.capabilities,
|
|
944
|
+
pullRequest: currentPullRequest, resource: { kind: 'pull-request', number: prNumber }, operations, resources: resourcesState,
|
|
945
|
+
evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: association.value },
|
|
946
|
+
error: { ...reread.error, code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${reread.error.message}` }
|
|
741
947
|
});
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
code: 'PR_INSPECTION_INVALID',
|
|
752
|
-
message: 'Platform adapter returned no change-request snapshot',
|
|
753
|
-
retryable: false
|
|
754
|
-
};
|
|
755
|
-
return result(error.retryable ? 'blocked' : 'failed', null, null, prNumber, {
|
|
756
|
-
platform: context.platform, capabilities: context.capabilities,
|
|
757
|
-
resource: { kind: 'pull-request', number: prNumber }, error
|
|
948
|
+
const after = reread.value.labels;
|
|
949
|
+
const state = resourcesState.find((resource) => resource.kind === item.kind && resource.number === item.number)!;
|
|
950
|
+
state.after = after;
|
|
951
|
+
state.effect = after.filter((label: string) => label.startsWith('in:')).sort().join('\0') === target.join('\0') ? 'applied' : 'unknown';
|
|
952
|
+
if (state.effect === 'unknown') return result('blocked', null, issueNumber, prNumber, {
|
|
953
|
+
changed: true, platform: context.platform, capabilities: context.capabilities,
|
|
954
|
+
pullRequest: currentPullRequest, resource: { kind: 'pull-request', number: prNumber }, operations, resources: resourcesState,
|
|
955
|
+
evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: association.value },
|
|
956
|
+
error: { code: 'IN_LABEL_SYNC_PARTIAL', message: `${item.kind} #${item.number} did not converge to the expected in: labels`, retryable: true }
|
|
758
957
|
});
|
|
958
|
+
if (item.kind === 'pull-request') currentPullRequest = reread.value as PullRequestSnapshot;
|
|
759
959
|
}
|
|
760
|
-
return result('no-op', null,
|
|
761
|
-
platform: context.platform, capabilities: context.capabilities,
|
|
762
|
-
resource: { kind: 'pull-request', number: prNumber },
|
|
960
|
+
return result(association.value.length !== 1 ? 'degraded' : successfulWrites > 0 ? 'applied' : 'no-op', null, issueNumber, prNumber, {
|
|
961
|
+
changed: successfulWrites > 0, platform: context.platform, capabilities: context.capabilities,
|
|
962
|
+
pullRequest: currentPullRequest, resource: { kind: 'pull-request', number: prNumber },
|
|
963
|
+
operations: operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' as const } : operation),
|
|
964
|
+
resources: resourcesState, evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: association.value }, error: null
|
|
763
965
|
});
|
|
764
966
|
}
|
|
765
967
|
|
|
@@ -770,9 +972,8 @@ function appendActivity(content: string, line: string): string {
|
|
|
770
972
|
|
|
771
973
|
function deliveryIdentity(pullRequest: PullRequestSnapshot) {
|
|
772
974
|
return {
|
|
975
|
+
resource: pullRequestIdentity(pullRequest),
|
|
773
976
|
repository: pullRequest.repository,
|
|
774
|
-
number: pullRequest.number,
|
|
775
|
-
nodeId: pullRequest.nodeId,
|
|
776
977
|
url: pullRequest.url,
|
|
777
978
|
head: { ...pullRequest.head },
|
|
778
979
|
base: { ...pullRequest.base }
|
|
@@ -786,13 +987,13 @@ function factTimestamp(timestamp: string): string {
|
|
|
786
987
|
function boundFactFor(
|
|
787
988
|
pullRequest: PullRequestSnapshot,
|
|
788
989
|
source: PrDeliveryBindingSource,
|
|
789
|
-
|
|
990
|
+
issue: number | ResourceIdentity | null,
|
|
790
991
|
verifiedAt: string
|
|
791
992
|
): PrDeliveryFact {
|
|
792
993
|
return buildBoundFact({
|
|
793
994
|
identity: deliveryIdentity(pullRequest),
|
|
794
995
|
source,
|
|
795
|
-
issueNumber,
|
|
996
|
+
...(typeof issue === 'object' ? { issueIdentity: issue } : { issueNumber: issue }),
|
|
796
997
|
verifiedAt: factTimestamp(verifiedAt),
|
|
797
998
|
remoteState: pullRequest.state,
|
|
798
999
|
mergedAt: pullRequest.state === 'closed' ? pullRequest.mergedAt : null,
|
|
@@ -800,7 +1001,7 @@ function boundFactFor(
|
|
|
800
1001
|
});
|
|
801
1002
|
}
|
|
802
1003
|
|
|
803
|
-
function writeCreateStarted(base: ReturnType<typeof resolvedContext
|
|
1004
|
+
function writeCreateStarted(base: Awaited<ReturnType<typeof resolvedContext>> & { ok: true }, agent: string) {
|
|
804
1005
|
const starts = (base.content.match(/\*\*Create PR \[started\]\*\*/g) || []).length;
|
|
805
1006
|
const dones = (base.content.match(/\*\*Create PR\*\*/g) || []).length;
|
|
806
1007
|
if (starts > dones) return { status: 'no-op' as const };
|
|
@@ -815,7 +1016,7 @@ function writeCreateStarted(base: ReturnType<typeof resolvedContext> & { ok: tru
|
|
|
815
1016
|
}
|
|
816
1017
|
|
|
817
1018
|
function bindIdentity(
|
|
818
|
-
base: ReturnType<typeof resolvedContext
|
|
1019
|
+
base: Awaited<ReturnType<typeof resolvedContext>> & { ok: true },
|
|
819
1020
|
pullRequest: PullRequestSnapshot,
|
|
820
1021
|
agent: string,
|
|
821
1022
|
source: PrDeliveryBindingSource,
|
|
@@ -829,10 +1030,10 @@ function bindIdentity(
|
|
|
829
1030
|
errorCode: 'PR_BIND_IDENTITY_MISMATCH'
|
|
830
1031
|
});
|
|
831
1032
|
if (!identity.ok) return { status: 'failed' as const, error: identity.error };
|
|
832
|
-
if (base.
|
|
833
|
-
code: 'PR_BIND_CONFLICT', message:
|
|
1033
|
+
if (base.prIdentity && !resourceIdentityEquals(base.prIdentity, pullRequestIdentity(pullRequest))) return { status: 'failed' as const, error: {
|
|
1034
|
+
code: 'PR_BIND_CONFLICT', message: 'Task is already bound to a different pull request identity'
|
|
834
1035
|
} };
|
|
835
|
-
if (base.
|
|
1036
|
+
if (base.prIdentity && resourceIdentityEquals(base.prIdentity, pullRequestIdentity(pullRequest)) && base.fact?.state === 'bound') {
|
|
836
1037
|
if (JSON.stringify(base.fact.identity) !== JSON.stringify(deliveryIdentity(pullRequest))) return { status: 'failed' as const, error: {
|
|
837
1038
|
code: 'PR_BIND_IDENTITY_MISMATCH', message: `Task binding for PR #${pullRequest.number} does not match the fetched pull request identity`
|
|
838
1039
|
} };
|
|
@@ -840,7 +1041,7 @@ function bindIdentity(
|
|
|
840
1041
|
}
|
|
841
1042
|
const current = fs.readFileSync(base.resolved.taskMdPath, 'utf8');
|
|
842
1043
|
const metadata = captureTaskWriteMetadata();
|
|
843
|
-
const fact = boundFactFor(pullRequest, source, base.
|
|
1044
|
+
const fact = boundFactFor(pullRequest, source, taskIssueIdentity(base.frontmatter), metadata.timestamp);
|
|
844
1045
|
const factMutation = factFrontmatterMutation(fact);
|
|
845
1046
|
return writeTask({
|
|
846
1047
|
taskRef: base.resolved.taskId,
|
|
@@ -855,64 +1056,59 @@ function bindIdentity(
|
|
|
855
1056
|
}, { repoRoot: base.resolved.repoRoot, metadataProvider: () => metadata });
|
|
856
1057
|
}
|
|
857
1058
|
|
|
858
|
-
function bindPlatformPullRequest(taskRef: string, options: BindOptions): PullRequestResult {
|
|
859
|
-
const base = resolvedContext(taskRef, options);
|
|
1059
|
+
async function bindPlatformPullRequest(taskRef: string, options: BindOptions): Promise<PullRequestResult> {
|
|
1060
|
+
const base = await resolvedContext(taskRef, options);
|
|
860
1061
|
if (!base.ok) return base.output;
|
|
861
1062
|
if (!base.fact) return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
862
1063
|
error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
|
|
863
1064
|
});
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
});
|
|
867
|
-
|
|
868
|
-
|
|
1065
|
+
let prIdentity: ResourceIdentity;
|
|
1066
|
+
try { prIdentity = providerResourceToken(base.provider, 'pull-request', String(options.pr)); }
|
|
1067
|
+
catch (error) { return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, { error: { code: 'PLATFORM_IDENTITY_TOKEN_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false } }); }
|
|
1068
|
+
const requestedNumber = resourceIdentityNumber(prIdentity);
|
|
1069
|
+
if (base.prIdentity && !resourceIdentityEquals(base.prIdentity, prIdentity)) return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1070
|
+
error: { code: 'PR_BIND_CONFLICT', message: 'Task is already bound to a different pull request identity', retryable: false }
|
|
869
1071
|
});
|
|
870
|
-
const
|
|
871
|
-
if (
|
|
872
|
-
const initialIdentity = validateWriterIdentity(base,
|
|
1072
|
+
const inspected = await inspectExternalPullRequest(base, prIdentity);
|
|
1073
|
+
if (inspected.status === 'failed' || inspected.status === 'blocked' || !inspected.pullRequest) return inspected;
|
|
1074
|
+
const initialIdentity = validateWriterIdentity(base, inspected.pullRequest, {
|
|
873
1075
|
expectedHead: String(base.frontmatter.branch || ''),
|
|
874
1076
|
errorCode: 'PR_BIND_IDENTITY_MISMATCH'
|
|
875
1077
|
});
|
|
876
|
-
if (!initialIdentity.ok) return result('failed', base.resolved.taskId, base.issueNumber,
|
|
877
|
-
pullRequest:
|
|
878
|
-
});
|
|
879
|
-
const rechecked = inspectGitHubPullRequest(base.client, base.context.platform.repository!, options.pr, base.resolved.repoRoot);
|
|
880
|
-
if (!rechecked.ok || !rechecked.value) return result(rechecked.ok ? 'failed' : rechecked.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
881
|
-
pullRequest: fetched.value,
|
|
882
|
-
error: rechecked.ok ? { code: 'PR_BIND_RECHECK_FAILED', message: 'Pull request recheck returned no identity', retryable: false } : rechecked.error
|
|
1078
|
+
if (!initialIdentity.ok) return result('failed', base.resolved.taskId, base.issueNumber, requestedNumber, {
|
|
1079
|
+
pullRequest: inspected.pullRequest, error: initialIdentity.error
|
|
883
1080
|
});
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
error: { code:
|
|
1081
|
+
const written = bindIdentity(base, inspected.pullRequest, options.agent, 'explicit-bind', options.dryRun);
|
|
1082
|
+
if (written.status === 'failed') return result('failed', base.resolved.taskId, base.issueNumber, requestedNumber, {
|
|
1083
|
+
pullRequest: inspected.pullRequest, error: { code: written.error.code, message: written.error.message, retryable: false }
|
|
887
1084
|
});
|
|
888
|
-
|
|
889
|
-
if (written.status === 'failed') return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
890
|
-
pullRequest: fetched.value, error: { code: written.error.code, message: written.error.message, retryable: false }
|
|
891
|
-
});
|
|
892
|
-
return result(options.dryRun ? 'planned' : written.status === 'no-op' ? 'no-op' : 'applied', base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber : rechecked.value.number, {
|
|
1085
|
+
return result(options.dryRun ? 'planned' : written.status === 'no-op' ? 'no-op' : 'applied', base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber : inspected.pullRequest.number, {
|
|
893
1086
|
changed: !options.dryRun && written.status === 'applied', platform: base.context.platform, capabilities: base.context.capabilities,
|
|
894
|
-
resource: { kind: 'pull-request', number:
|
|
1087
|
+
resource: { kind: 'pull-request', number: inspected.pullRequest.number, identity: pullRequestIdentity(inspected.pullRequest) }, pullRequest: inspected.pullRequest,
|
|
895
1088
|
operations: [{ name: 'task:bind-pr', status: options.dryRun ? 'planned' : written.status === 'no-op' ? 'no-op' : 'applied', reasonCode: null }], error: null
|
|
896
1089
|
});
|
|
897
1090
|
}
|
|
898
1091
|
|
|
899
1092
|
function externalEvidenceNote(
|
|
900
|
-
|
|
1093
|
+
issueIdentity: ResourceIdentity | null,
|
|
901
1094
|
source: 'unique' | 'explicit',
|
|
902
1095
|
pullRequest: PullRequestSnapshot
|
|
903
1096
|
): string {
|
|
904
1097
|
return [
|
|
905
1098
|
`authorization=${source}`,
|
|
906
|
-
externalIdentityEvidenceNote(
|
|
1099
|
+
externalIdentityEvidenceNote(issueIdentity, pullRequest)
|
|
907
1100
|
].join('; ');
|
|
908
1101
|
}
|
|
909
1102
|
|
|
910
1103
|
function externalIdentityEvidenceNote(
|
|
911
|
-
|
|
1104
|
+
issueIdentity: ResourceIdentity | null,
|
|
912
1105
|
pullRequest: PullRequestSnapshot
|
|
913
1106
|
): string {
|
|
1107
|
+
const issueLabel = issueIdentity
|
|
1108
|
+
? issueIdentity.kind === 'number' ? `#${issueIdentity.value}` : `${issueIdentity.kind}:${issueIdentity.value}`
|
|
1109
|
+
: 'unknown';
|
|
914
1110
|
return [
|
|
915
|
-
`issue
|
|
1111
|
+
`issue=${issueLabel}`,
|
|
916
1112
|
`pr=#${pullRequest.number}`,
|
|
917
1113
|
`url=${pullRequest.url}`,
|
|
918
1114
|
`base=${pullRequest.base.repository}:${pullRequest.base.ref}@${pullRequest.base.sha}`,
|
|
@@ -922,8 +1118,8 @@ function externalIdentityEvidenceNote(
|
|
|
922
1118
|
].join('; ');
|
|
923
1119
|
}
|
|
924
1120
|
|
|
925
|
-
function resolveExternalPullRequest(taskRef: string, options: ResolveExternalOptions): ExternalPullRequestResult {
|
|
926
|
-
const base = resolvedContext(taskRef, options);
|
|
1121
|
+
async function resolveExternalPullRequest(taskRef: string, options: ResolveExternalOptions): Promise<ExternalPullRequestResult> {
|
|
1122
|
+
const base = await resolvedContext(taskRef, options);
|
|
927
1123
|
if (!base.ok) return externalResult(base.output);
|
|
928
1124
|
if (!base.fact) return externalResult(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
929
1125
|
error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
|
|
@@ -947,28 +1143,41 @@ function resolveExternalPullRequest(taskRef: string, options: ResolveExternalOpt
|
|
|
947
1143
|
error: null
|
|
948
1144
|
}), { mode: 'normal' });
|
|
949
1145
|
}
|
|
950
|
-
if (!base.
|
|
1146
|
+
if (!base.issueIdentity) return externalResult(result('failed', base.resolved.taskId, null, base.prNumber, {
|
|
951
1147
|
platform: base.context.platform,
|
|
952
1148
|
capabilities: base.context.capabilities,
|
|
953
1149
|
error: { code: 'EXTERNAL_DELIVERY_ISSUE_REQUIRED', message: 'External delivery requires a valid issue_number', retryable: false }
|
|
954
1150
|
}));
|
|
955
|
-
const inspected =
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1151
|
+
const inspected = base.provider.changeRequests?.listClosing
|
|
1152
|
+
? await base.provider.changeRequests.listClosing({
|
|
1153
|
+
context: providerOperationContext(base.loadedContext),
|
|
1154
|
+
issue: base.issueIdentity
|
|
1155
|
+
})
|
|
1156
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.listClosing');
|
|
961
1157
|
if (!inspected.ok || !inspected.value) {
|
|
962
|
-
const error =
|
|
1158
|
+
const error = 'error' in inspected && inspected.error
|
|
1159
|
+
? inspected.error
|
|
1160
|
+
: { code: 'PR_IDENTITY_INVALID', message: 'Closing PR inspection returned no candidates', retryable: false };
|
|
963
1161
|
return externalResult(result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
964
1162
|
platform: base.context.platform, capabilities: base.context.capabilities, error
|
|
965
1163
|
}));
|
|
966
1164
|
}
|
|
1165
|
+
const candidates = (inspected.value as ProviderChangeRequestSnapshot[]).map((item) => normalizeProviderPullRequest(item, base.context.platform.repository!, item.number || 0));
|
|
1166
|
+
let explicitPrIdentity: ResourceIdentity | null = null;
|
|
1167
|
+
if (options.pr !== undefined) {
|
|
1168
|
+
try { explicitPrIdentity = providerResourceToken(base.provider, 'pull-request', String(options.pr)); }
|
|
1169
|
+
catch (error) {
|
|
1170
|
+
return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1171
|
+
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1172
|
+
error: { code: 'PLATFORM_IDENTITY_TOKEN_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
1173
|
+
}));
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
967
1176
|
const selected = selectExternalPullRequest(
|
|
968
|
-
|
|
1177
|
+
candidates,
|
|
969
1178
|
base.context.platform.repository!,
|
|
970
|
-
base.
|
|
971
|
-
|
|
1179
|
+
base.prIdentity,
|
|
1180
|
+
explicitPrIdentity
|
|
972
1181
|
);
|
|
973
1182
|
if (selected.status === 'normal') return externalResult(result('no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
974
1183
|
platform: base.context.platform,
|
|
@@ -987,7 +1196,14 @@ function resolveExternalPullRequest(taskRef: string, options: ResolveExternalOpt
|
|
|
987
1196
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
988
1197
|
pullRequest: selected.selected, error: initialIdentity.error
|
|
989
1198
|
}), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
|
|
990
|
-
const rechecked =
|
|
1199
|
+
const rechecked = base.provider.changeRequests?.inspect
|
|
1200
|
+
? await base.provider.changeRequests.inspect({
|
|
1201
|
+
context: providerOperationContext(base.loadedContext),
|
|
1202
|
+
target: pullRequestIdentity(selected.selected)
|
|
1203
|
+
}).then((response) => response.ok
|
|
1204
|
+
? { ok: true as const, value: normalizeProviderPullRequest(response.value, base.context.platform.repository!, selected.selected.number, selected.selected) }
|
|
1205
|
+
: response)
|
|
1206
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
|
|
991
1207
|
if (!rechecked.ok || !rechecked.value) return externalResult(result(rechecked.ok ? 'failed' : rechecked.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
992
1208
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
993
1209
|
pullRequest: selected.selected,
|
|
@@ -1009,8 +1225,8 @@ function resolveExternalPullRequest(taskRef: string, options: ResolveExternalOpt
|
|
|
1009
1225
|
pullRequest: rechecked.value, error: finalEvidence.error
|
|
1010
1226
|
}), { candidates: selected.candidates, eligible: selected.eligible, selected: rechecked.value });
|
|
1011
1227
|
const selectedPullRequest = rechecked.value;
|
|
1012
|
-
const note = externalEvidenceNote(base.
|
|
1013
|
-
const identityNote = externalIdentityEvidenceNote(base.
|
|
1228
|
+
const note = externalEvidenceNote(base.issueIdentity, selected.source, selectedPullRequest);
|
|
1229
|
+
const identityNote = externalIdentityEvidenceNote(base.issueIdentity, selectedPullRequest);
|
|
1014
1230
|
const activityEntry = `**Bind External PR** by ${options.agent} — ${note}`;
|
|
1015
1231
|
const auditLines = base.content.split(/\r?\n/).filter((line) => line.includes('**Bind External PR**'));
|
|
1016
1232
|
const alreadyAudited = auditLines.some((line) => line.endsWith(identityNote));
|
|
@@ -1020,13 +1236,13 @@ function resolveExternalPullRequest(taskRef: string, options: ResolveExternalOpt
|
|
|
1020
1236
|
pullRequest: selectedPullRequest,
|
|
1021
1237
|
error: { code: 'PR_BIND_CONFLICT', message: 'Existing external PR audit evidence conflicts with the selected identity', retryable: false }
|
|
1022
1238
|
}), { candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
|
|
1023
|
-
if (alreadyAudited && base.
|
|
1239
|
+
if (alreadyAudited && base.prIdentity && !resourceIdentityEquals(base.prIdentity, pullRequestIdentity(selectedPullRequest))) return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1024
1240
|
platform: base.context.platform,
|
|
1025
1241
|
capabilities: base.context.capabilities,
|
|
1026
1242
|
pullRequest: selectedPullRequest,
|
|
1027
1243
|
error: { code: 'PR_BIND_CONFLICT', message: 'External PR audit evidence does not match the task binding', retryable: false }
|
|
1028
1244
|
}), { candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
|
|
1029
|
-
if (base.
|
|
1245
|
+
if (base.prIdentity && resourceIdentityEquals(base.prIdentity, pullRequestIdentity(selectedPullRequest)) && alreadyAudited) {
|
|
1030
1246
|
return externalResult(result('no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1031
1247
|
platform: base.context.platform,
|
|
1032
1248
|
capabilities: base.context.capabilities,
|
|
@@ -1066,167 +1282,20 @@ function resolveExternalPullRequest(taskRef: string, options: ResolveExternalOpt
|
|
|
1066
1282
|
}), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
|
|
1067
1283
|
}
|
|
1068
1284
|
|
|
1069
|
-
function
|
|
1070
|
-
const base = resolvedContext(taskRef, options);
|
|
1071
|
-
if (!base.ok) return withCreation(base.output, PRECONDITION_NOT_CREATED);
|
|
1072
|
-
if (!base.fact) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1073
|
-
error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
|
|
1074
|
-
}), PRECONDITION_NOT_CREATED);
|
|
1075
|
-
if (!options.base || !options.head || !options.title.trim() || !options.body.trim()) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1076
|
-
error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
|
|
1077
|
-
}), PRECONDITION_NOT_CREATED);
|
|
1078
|
-
const target = taskDeliveryTarget(base);
|
|
1079
|
-
if (!target.ok) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1080
|
-
error: target.error
|
|
1081
|
-
}), PRECONDITION_NOT_CREATED);
|
|
1082
|
-
if (options.base !== target.value.baseRef) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1083
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1084
|
-
error: { code: 'PR_DELIVERY_TARGET_MISMATCH', message: `Pull request base '${options.base}' does not match delivery target '${target.value.baseRef}'`, retryable: false }
|
|
1085
|
-
}), PRECONDITION_NOT_CREATED);
|
|
1086
|
-
if (base.prNumber) {
|
|
1087
|
-
const headCheck = verifyCreateHead(base, options.head);
|
|
1088
|
-
if (!headCheck.ok) return withCreation(result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1089
|
-
platform: base.context.platform,
|
|
1090
|
-
capabilities: base.context.capabilities,
|
|
1091
|
-
error: headCheck.error
|
|
1092
|
-
}), PRECONDITION_NOT_CREATED);
|
|
1093
|
-
const inspected = inspectPlatformPullRequest(taskRef, options);
|
|
1094
|
-
const identity = validateBoundPullRequest(base, inspected.pullRequest, options.head, options.base, headCheck.value);
|
|
1095
|
-
if (!identity.ok) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1096
|
-
platform: base.context.platform,
|
|
1097
|
-
capabilities: base.context.capabilities,
|
|
1098
|
-
pullRequest: inspected.pullRequest,
|
|
1099
|
-
error: identity.error
|
|
1100
|
-
}), PRECONDITION_NOT_CREATED);
|
|
1101
|
-
return withCreation({ ...inspected, result: 'no_op' }, { kind: 'no-op', createdByCurrentOperation: false });
|
|
1102
|
-
}
|
|
1103
|
-
const headCheck = verifyCreateHead(base, options.head);
|
|
1104
|
-
if (!headCheck.ok) return withCreation(result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1105
|
-
platform: base.context.platform,
|
|
1106
|
-
capabilities: base.context.capabilities,
|
|
1107
|
-
error: headCheck.error
|
|
1108
|
-
}), PRECONDITION_NOT_CREATED);
|
|
1109
|
-
const expectedHeadSha = headCheck.value;
|
|
1110
|
-
const located = locatePullRequest(base, options.head, options.base, expectedHeadSha);
|
|
1111
|
-
if (!located.ok && located.error.code === 'PR_IDENTITY_AMBIGUOUS') return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1112
|
-
platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
|
|
1113
|
-
}), PRECONDITION_NOT_CREATED);
|
|
1114
|
-
if (!located.ok && located.error.code === 'PR_HEAD_SHA_MISMATCH') return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1115
|
-
platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
|
|
1116
|
-
}), PRECONDITION_NOT_CREATED);
|
|
1117
|
-
if (!located.ok && located.error.code !== 'PR_NOT_FOUND') return withCreation(result(located.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1118
|
-
platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
|
|
1119
|
-
}), PRECONDITION_NOT_CREATED);
|
|
1120
|
-
if (options.dryRun) return withCreation(result('planned', base.resolved.taskId, base.issueNumber, null, {
|
|
1121
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1122
|
-
operations: [{ name: located.ok ? 'pr:reuse' : 'pr:create', status: 'planned', reasonCode: null }],
|
|
1123
|
-
result: located.ok ? 'pr_reused' : 'pr_created', error: null
|
|
1124
|
-
}), { kind: 'planned', action: located.ok ? 'reuse' : 'create', createdByCurrentOperation: false });
|
|
1125
|
-
const started = writeCreateStarted(base, options.agent);
|
|
1126
|
-
if (started.status === 'failed') return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1127
|
-
error: { code: started.error.code, message: started.error.message, retryable: false }
|
|
1128
|
-
}), PRECONDITION_NOT_CREATED);
|
|
1129
|
-
let pullRequest = located.ok ? located.value : null;
|
|
1130
|
-
let created = false;
|
|
1131
|
-
if (!pullRequest) {
|
|
1132
|
-
const repository = base.context.platform.repository!;
|
|
1133
|
-
if (options.phase) options.phase.value = 'post-dispatched';
|
|
1134
|
-
const response = base.client.json<RemotePullRequest>(['api', `repos/${repository}/pulls`, '-X', 'POST', '--input', '-'], {
|
|
1135
|
-
cwd: base.resolved.repoRoot,
|
|
1136
|
-
method: 'POST',
|
|
1137
|
-
input: JSON.stringify({ title: options.title, body: options.body, head: options.head, base: options.base, draft: Boolean(options.draft) })
|
|
1138
|
-
});
|
|
1139
|
-
if (response.ok) {
|
|
1140
|
-
// The remote POST is accepted before any normalization, recheck, or task write.
|
|
1141
|
-
// From this point on the current call has created a remote resource even if
|
|
1142
|
-
// local post-processing later fails.
|
|
1143
|
-
created = true;
|
|
1144
|
-
if (options.phase) options.phase.value = 'post-accepted';
|
|
1145
|
-
pullRequest = normalizePullRequest(response.value, repository);
|
|
1146
|
-
} else if (response.error.retryable) {
|
|
1147
|
-
return withCreation(result('blocked', base.resolved.taskId, base.issueNumber, null, {
|
|
1148
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1149
|
-
operations: [{ name: 'pr:create', status: 'failed', reasonCode: 'PR_CREATE_OUTCOME_UNKNOWN' }],
|
|
1150
|
-
error: { code: 'PR_CREATE_OUTCOME_UNKNOWN', message: response.error.message, retryable: true }
|
|
1151
|
-
}), { kind: 'unknown', errorCode: 'PR_CREATE_OUTCOME_UNKNOWN' });
|
|
1152
|
-
} else return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1153
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1154
|
-
operations: [{ name: 'pr:create', status: 'failed', reasonCode: response.error.code }], error: response.error
|
|
1155
|
-
}), { kind: 'not-created', reason: 'post-rejected', createdByCurrentOperation: false });
|
|
1156
|
-
if (!pullRequest) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1157
|
-
error: { code: 'PR_CREATE_RESPONSE_INVALID', message: 'PR create response lacks validated identity', retryable: false }
|
|
1158
|
-
}), { kind: 'created', createdByCurrentOperation: true });
|
|
1159
|
-
if (pullRequest.head.sha !== expectedHeadSha) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1160
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1161
|
-
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
|
|
1162
|
-
error: { code: 'PR_HEAD_SHA_MISMATCH', message: 'Created pull request head does not match the expected local HEAD', retryable: false }
|
|
1163
|
-
}), { kind: 'created', createdByCurrentOperation: true });
|
|
1164
|
-
}
|
|
1165
|
-
const bindHeadCheck = verifyCreateHead(base, options.head);
|
|
1166
|
-
if (!bindHeadCheck.ok || bindHeadCheck.value !== expectedHeadSha) {
|
|
1167
|
-
const error = bindHeadCheck.ok
|
|
1168
|
-
? { code: 'PR_REMOTE_HEAD_MISMATCH', message: 'Remote head changed before task binding', retryable: false }
|
|
1169
|
-
: bindHeadCheck.error;
|
|
1170
|
-
return withCreation(result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1171
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1172
|
-
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest, error
|
|
1173
|
-
}), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false });
|
|
1174
|
-
}
|
|
1175
|
-
const beforeBind = inspectGitHubPullRequest(base.client, base.context.platform.repository!, pullRequest.number, base.resolved.repoRoot);
|
|
1176
|
-
if (!beforeBind.ok || !beforeBind.value) {
|
|
1177
|
-
const error = beforeBind.ok
|
|
1178
|
-
? { code: 'PR_BIND_RECHECK_FAILED', message: 'Pull request recheck returned no identity', retryable: false }
|
|
1179
|
-
: beforeBind.error;
|
|
1180
|
-
return withCreation(result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1181
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1182
|
-
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest, error
|
|
1183
|
-
}), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false });
|
|
1184
|
-
}
|
|
1185
|
-
if (
|
|
1186
|
-
beforeBind.value.head.sha !== expectedHeadSha
|
|
1187
|
-
|| beforeBind.value.head.ref !== expectedHead(base.context.platform.repository!, options.head).ref
|
|
1188
|
-
|| beforeBind.value.base.ref !== options.base
|
|
1189
|
-
|| beforeBind.value.base.repository !== base.context.platform.repository
|
|
1190
|
-
) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1191
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1192
|
-
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest: beforeBind.value,
|
|
1193
|
-
error: { code: 'PR_BIND_IDENTITY_MISMATCH', message: 'Pull request identity changed before task binding', retryable: false }
|
|
1194
|
-
}), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false });
|
|
1195
|
-
pullRequest = beforeBind.value;
|
|
1196
|
-
const refreshed = resolvedContext(taskRef, options);
|
|
1197
|
-
if (!refreshed.ok) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1198
|
-
pullRequest, error: { code: 'PR_CREATED_BIND_FAILED', message: pullRequest.url, retryable: false }
|
|
1199
|
-
}), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false });
|
|
1200
|
-
const bound = bindIdentity(refreshed, pullRequest, options.agent, created ? 'created' : 'reused', false, options.head, expectedHeadSha);
|
|
1201
|
-
if (bound.status === 'failed') return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1202
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1203
|
-
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
|
|
1204
|
-
operations: [{ name: created ? 'pr:create' : 'pr:reuse', status: 'applied', reasonCode: null }, { name: 'task:bind-pr', status: 'failed', reasonCode: bound.error.code }],
|
|
1205
|
-
error: { code: 'PR_CREATED_BIND_FAILED', message: `${pullRequest.url}: ${bound.error.message}`, retryable: false }
|
|
1206
|
-
}), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'reused', createdByCurrentOperation: false });
|
|
1207
|
-
return withCreation(result('applied', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1208
|
-
changed: true, platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1209
|
-
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
|
|
1210
|
-
operations: [{ name: created ? 'pr:create' : 'pr:reuse', status: created ? 'applied' : 'no-op', reasonCode: null }, { name: 'task:bind-pr', status: 'applied', reasonCode: null }],
|
|
1211
|
-
result: created ? 'pr_created' : 'pr_reused', error: null
|
|
1212
|
-
}), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'reused', createdByCurrentOperation: false });
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
function createPlatformPullRequest(taskRef: string, options: CreateOptions): PullRequestResult {
|
|
1216
|
-
const base = resolvedContext(taskRef, options);
|
|
1285
|
+
async function createPlatformPullRequest(taskRef: string, options: CreateOptions): Promise<PullRequestResult> {
|
|
1286
|
+
const base = await resolvedContext(taskRef, options);
|
|
1217
1287
|
if (!base.ok) return withCreation(base.output, PRECONDITION_NOT_CREATED);
|
|
1218
1288
|
if (!options.base || !options.head || !options.title.trim() || !options.body.trim()) {
|
|
1219
1289
|
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1220
1290
|
error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
|
|
1221
1291
|
}), PRECONDITION_NOT_CREATED);
|
|
1222
1292
|
}
|
|
1223
|
-
const phase = { value: 'before-post' as CreatePhase };
|
|
1224
1293
|
try {
|
|
1225
1294
|
return withTaskExecutionLock(
|
|
1226
1295
|
base.resolved.repoRoot,
|
|
1227
1296
|
base.resolved.taskId,
|
|
1228
1297
|
'platform-pr.create',
|
|
1229
|
-
() =>
|
|
1298
|
+
() => createExternalPullRequest(taskRef, options, base)
|
|
1230
1299
|
);
|
|
1231
1300
|
} catch (error) {
|
|
1232
1301
|
if (error instanceof TaskExecutionLockError) {
|
|
@@ -1236,27 +1305,154 @@ function createPlatformPullRequest(taskRef: string, options: CreateOptions): Pul
|
|
|
1236
1305
|
error: { code: error.code, message: error.message, retryable: true }
|
|
1237
1306
|
}), PRECONDITION_NOT_CREATED);
|
|
1238
1307
|
}
|
|
1239
|
-
|
|
1240
|
-
? { kind: 'created', createdByCurrentOperation: true }
|
|
1241
|
-
: phase.value === 'post-dispatched'
|
|
1242
|
-
? { kind: 'unknown', errorCode: 'PR_CREATE_OUTCOME_UNKNOWN' }
|
|
1243
|
-
: PRECONDITION_NOT_CREATED;
|
|
1244
|
-
return withCreation(result(phase.value === 'post-dispatched' ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1308
|
+
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1245
1309
|
platform: base.context.platform,
|
|
1246
1310
|
capabilities: base.context.capabilities,
|
|
1247
1311
|
error: {
|
|
1248
|
-
code:
|
|
1312
|
+
code: 'PR_CREATE_FAILED',
|
|
1249
1313
|
message: error instanceof Error ? error.message : String(error),
|
|
1250
|
-
retryable:
|
|
1314
|
+
retryable: false
|
|
1251
1315
|
}
|
|
1252
|
-
}),
|
|
1316
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1253
1317
|
}
|
|
1254
1318
|
}
|
|
1255
1319
|
|
|
1256
|
-
function
|
|
1320
|
+
async function createExternalPullRequest(
|
|
1321
|
+
taskRef: string,
|
|
1322
|
+
options: CreateOptions,
|
|
1323
|
+
base: Extract<Awaited<ReturnType<typeof resolvedContext>>, { ok: true }>
|
|
1324
|
+
): Promise<PullRequestResult> {
|
|
1325
|
+
if (!base.fact) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1326
|
+
error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
|
|
1327
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1328
|
+
const target = taskDeliveryTarget(base);
|
|
1329
|
+
if (!target.ok) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1330
|
+
error: target.error
|
|
1331
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1332
|
+
if (options.base !== target.value.baseRef) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1333
|
+
platform: base.context.platform,
|
|
1334
|
+
capabilities: base.context.capabilities,
|
|
1335
|
+
error: { code: 'PR_DELIVERY_TARGET_MISMATCH', message: `Pull request base '${options.base}' does not match delivery target '${target.value.baseRef}'`, retryable: false }
|
|
1336
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1337
|
+
let verifiedHeadSha: string | undefined;
|
|
1338
|
+
if (base.provider.changeRequests?.verifyHead) {
|
|
1339
|
+
const verified = await base.provider.changeRequests.verifyHead({
|
|
1340
|
+
context: providerOperationContext(base.loadedContext),
|
|
1341
|
+
head: options.head
|
|
1342
|
+
});
|
|
1343
|
+
if (!verified.ok) return withCreation(providerPullRequestError(base, verified.error, base.prNumber), PRECONDITION_NOT_CREATED);
|
|
1344
|
+
verifiedHeadSha = verified.value.sha;
|
|
1345
|
+
}
|
|
1346
|
+
if (base.prIdentity) {
|
|
1347
|
+
const inspected = await inspectExternalPullRequest(base, base.prIdentity!);
|
|
1348
|
+
return inspected.status === 'failed' || inspected.status === 'blocked'
|
|
1349
|
+
? withCreation(inspected, PRECONDITION_NOT_CREATED)
|
|
1350
|
+
: withCreation({ ...inspected, result: 'no_op' }, { kind: 'no-op', createdByCurrentOperation: false });
|
|
1351
|
+
}
|
|
1352
|
+
if (options.dryRun) return withCreation(result('planned', base.resolved.taskId, base.issueNumber, null, {
|
|
1353
|
+
platform: base.context.platform,
|
|
1354
|
+
capabilities: base.context.capabilities,
|
|
1355
|
+
operations: [{ name: 'pr:create', status: 'planned', reasonCode: null }],
|
|
1356
|
+
result: 'pr_created',
|
|
1357
|
+
error: null
|
|
1358
|
+
}), { kind: 'planned', action: 'create', createdByCurrentOperation: false });
|
|
1359
|
+
|
|
1360
|
+
const started = writeCreateStarted(base, options.agent);
|
|
1361
|
+
if (started.status === 'failed') return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1362
|
+
error: { code: started.error.code, message: started.error.message, retryable: false }
|
|
1363
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1364
|
+
const created = base.provider.changeRequests?.create
|
|
1365
|
+
? await base.provider.changeRequests.create({
|
|
1366
|
+
context: providerOperationContext(base.loadedContext),
|
|
1367
|
+
base: options.base,
|
|
1368
|
+
head: options.head,
|
|
1369
|
+
title: options.title,
|
|
1370
|
+
body: options.body,
|
|
1371
|
+
draft: Boolean(options.draft),
|
|
1372
|
+
mutation: { idempotencyKey: `pull-request:create:${base.resolved.taskId}` }
|
|
1373
|
+
})
|
|
1374
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.create');
|
|
1375
|
+
if (!created.ok) return withCreation(providerPullRequestError(base, created.error, null), { kind: 'unknown', errorCode: 'PR_CREATE_OUTCOME_UNKNOWN' });
|
|
1376
|
+
const remoteId = String(created.value.remoteId || '');
|
|
1377
|
+
let createdIdentity: ResourceIdentity;
|
|
1378
|
+
try { createdIdentity = providerResourceIdentity(base.provider, 'pull-request', remoteId); }
|
|
1379
|
+
catch (error) {
|
|
1380
|
+
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1381
|
+
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1382
|
+
error: { code: 'PR_CREATE_RESPONSE_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
1383
|
+
}), { kind: 'created', createdByCurrentOperation: true });
|
|
1384
|
+
}
|
|
1385
|
+
const number = resourceIdentityNumber(createdIdentity);
|
|
1386
|
+
const fallbackNumber = number ?? 0;
|
|
1387
|
+
const fallback = normalizeProviderPullRequest({
|
|
1388
|
+
id: remoteId,
|
|
1389
|
+
identity: createdIdentity,
|
|
1390
|
+
number: number || undefined,
|
|
1391
|
+
state: 'open',
|
|
1392
|
+
title: options.title,
|
|
1393
|
+
body: options.body,
|
|
1394
|
+
headSha: '',
|
|
1395
|
+
baseSha: '',
|
|
1396
|
+
head: { repository: base.context.platform.repository!, ref: options.head, sha: '' },
|
|
1397
|
+
base: { repository: base.context.platform.repository!, ref: options.base, sha: '' },
|
|
1398
|
+
displayUrl: `${base.provider.type}:${remoteId}`
|
|
1399
|
+
}, base.context.platform.repository!, fallbackNumber);
|
|
1400
|
+
const inspected = base.provider.changeRequests?.inspect
|
|
1401
|
+
? await base.provider.changeRequests.inspect({
|
|
1402
|
+
context: providerOperationContext(base.loadedContext),
|
|
1403
|
+
target: createdIdentity
|
|
1404
|
+
})
|
|
1405
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
|
|
1406
|
+
if (!inspected.ok) return withCreation(providerPullRequestError(base, inspected.error, number), { kind: 'created', createdByCurrentOperation: true });
|
|
1407
|
+
const pullRequest = normalizeProviderPullRequest(
|
|
1408
|
+
inspected.value,
|
|
1409
|
+
base.context.platform.repository!,
|
|
1410
|
+
inspected.value.number ?? fallback.number,
|
|
1411
|
+
fallback
|
|
1412
|
+
);
|
|
1413
|
+
if (!pullRequest.nodeId || !isUsableResourceIdentity(pullRequestIdentity(pullRequest))) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1414
|
+
platform: base.context.platform,
|
|
1415
|
+
capabilities: base.context.capabilities,
|
|
1416
|
+
error: { code: 'PR_CREATE_RESPONSE_INVALID', message: 'Provider create response lacks validated identity', retryable: false }
|
|
1417
|
+
}), { kind: 'created', createdByCurrentOperation: true });
|
|
1418
|
+
const identity = validateWriterIdentity(base, pullRequest, {
|
|
1419
|
+
expectedHead: options.head,
|
|
1420
|
+
expectedHeadSha: verifiedHeadSha,
|
|
1421
|
+
errorCode: 'PR_BIND_IDENTITY_MISMATCH'
|
|
1422
|
+
});
|
|
1423
|
+
if (!identity.ok) return withCreation(result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1424
|
+
platform: base.context.platform,
|
|
1425
|
+
capabilities: base.context.capabilities,
|
|
1426
|
+
resource: { kind: 'pull-request', number: pullRequest.number },
|
|
1427
|
+
pullRequest,
|
|
1428
|
+
error: identity.error
|
|
1429
|
+
}), { kind: 'created', createdByCurrentOperation: true });
|
|
1430
|
+
const bound = bindIdentity(base, pullRequest, options.agent, 'created', false, options.head);
|
|
1431
|
+
if (bound.status === 'failed') return withCreation(result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1432
|
+
platform: base.context.platform,
|
|
1433
|
+
capabilities: base.context.capabilities,
|
|
1434
|
+
resource: { kind: 'pull-request', number: pullRequest.number },
|
|
1435
|
+
pullRequest,
|
|
1436
|
+
error: { code: 'PR_CREATED_BIND_FAILED', message: `${pullRequest.url}: ${bound.error.message}`, retryable: false }
|
|
1437
|
+
}), { kind: 'created', createdByCurrentOperation: true });
|
|
1438
|
+
return withCreation(result('applied', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1439
|
+
changed: true,
|
|
1440
|
+
platform: base.context.platform,
|
|
1441
|
+
capabilities: base.context.capabilities,
|
|
1442
|
+
resource: { kind: 'pull-request', number: pullRequest.number },
|
|
1443
|
+
pullRequest,
|
|
1444
|
+
operations: [{ name: 'pr:create', status: 'applied', reasonCode: null }, { name: 'task:bind-pr', status: 'applied', reasonCode: null }],
|
|
1445
|
+
result: 'pr_created',
|
|
1446
|
+
error: null
|
|
1447
|
+
}), { kind: 'created', createdByCurrentOperation: true });
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
async function syncPlatformPullRequest(taskRef: string, options: SyncOptions): Promise<PullRequestResult> {
|
|
1257
1451
|
const warningResult = warningResultForPrimary(options.primaryResult);
|
|
1258
1452
|
const softenFailure = (output: PullRequestResult): PullRequestResult => {
|
|
1453
|
+
const authorityError = output.error?.code.startsWith('IN_LABEL_SYNC') || output.error?.code.startsWith('PR_IDENTITY');
|
|
1259
1454
|
const warning = output.error && output.task.prNumber !== null
|
|
1455
|
+
&& !authorityError
|
|
1260
1456
|
&& !['PR_NOT_LINKED', 'PR_BIND_CONFLICT'].includes(output.error.code)
|
|
1261
1457
|
? {
|
|
1262
1458
|
code: output.error.code,
|
|
@@ -1271,66 +1467,148 @@ function syncPlatformPullRequest(taskRef: string, options: SyncOptions): PullReq
|
|
|
1271
1467
|
? { ...output, status: 'applied', changed: false, error: null, result: warningResult, warnings: mergeOperationWarnings([warning]) }
|
|
1272
1468
|
: output;
|
|
1273
1469
|
};
|
|
1274
|
-
const base = resolvedContext(taskRef, options);
|
|
1470
|
+
const base = await resolvedContext(taskRef, options);
|
|
1275
1471
|
if (!base.ok) return softenFailure(base.output);
|
|
1276
1472
|
if (!base.fact) return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1277
1473
|
error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
|
|
1278
1474
|
}));
|
|
1279
|
-
if (!base.
|
|
1475
|
+
if (!base.prIdentity) return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1280
1476
|
error: { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false }
|
|
1281
1477
|
}));
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
const payload: Record<string, unknown> = {};
|
|
1307
|
-
for (const operation of planned) {
|
|
1308
|
-
if (operation.status !== 'planned') continue;
|
|
1309
|
-
if (operation.name === 'labels') payload.labels = operation.value;
|
|
1310
|
-
if (operation.name === 'assignees') payload.assignees = operation.value;
|
|
1311
|
-
if (operation.name === 'closing-issue') payload.body = operation.value;
|
|
1312
|
-
if (operation.name === 'milestone') {
|
|
1313
|
-
const milestones = base.client.json<unknown[]>(['api', '--paginate', '--slurp', `repos/${base.context.platform.repository}/milestones?state=open&per_page=100`], { cwd: base.resolved.repoRoot });
|
|
1314
|
-
if (!milestones.ok) return softenFailure(result(milestones.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { pullRequest: fetched.value, operations, error: milestones.error }));
|
|
1315
|
-
const flat = milestones.value.flatMap((entry) => Array.isArray(entry) ? entry : [entry]) as Array<{ title?: string; number?: number }>;
|
|
1316
|
-
payload.milestone = flat.find((entry) => entry.title === operation.value)?.number ?? null;
|
|
1478
|
+
{
|
|
1479
|
+
const inspected = await inspectExternalPullRequest(base, base.prIdentity!);
|
|
1480
|
+
if (inspected.status === 'failed' || inspected.status === 'blocked' || !inspected.pullRequest) return softenFailure(inspected);
|
|
1481
|
+
if (!options.metadata && !options.closingIssue) return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1482
|
+
error: { code: 'PR_PAYLOAD_INVALID', message: 'sync requires a desired-state option', retryable: false }
|
|
1483
|
+
}));
|
|
1484
|
+
const issue = await inspectPlatformIssue(taskRef, { cwd: base.resolved.repoRoot, client: base.client });
|
|
1485
|
+
if (!issue.issue) return softenFailure(result(issue.status, base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1486
|
+
platform: base.context.platform, capabilities: base.context.capabilities, pullRequest: inspected.pullRequest, error: issue.error
|
|
1487
|
+
}));
|
|
1488
|
+
let inLabels = inspected.pullRequest.labels.filter((label) => label.startsWith('in:')).sort();
|
|
1489
|
+
let issueInLabelOperation: PlatformOperation | null = null;
|
|
1490
|
+
if (options.metadata && base.provider.issues?.listLabels) {
|
|
1491
|
+
const labels = await base.provider.issues.listLabels({ context: providerOperationContext(base.loadedContext) });
|
|
1492
|
+
if (!labels.ok) return softenFailure(providerPullRequestError(base, labels.error, base.prNumber));
|
|
1493
|
+
const target = taskInLabelTarget(base.resolved.repoRoot, base.frontmatter, inspected.pullRequest.labels, new Set(labels.value));
|
|
1494
|
+
if (!target.ok) return softenFailure(providerPullRequestError(base, target.error, base.prNumber));
|
|
1495
|
+
inLabels = target.value;
|
|
1496
|
+
const currentIssueLabels = issue.issue.labels.filter((label) => label.startsWith('in:')).sort();
|
|
1497
|
+
issueInLabelOperation = base.context.capabilities.triage
|
|
1498
|
+
? currentIssueLabels.join('\0') === inLabels.join('\0')
|
|
1499
|
+
? { name: 'labels:in:issue', status: 'no-op', reasonCode: null }
|
|
1500
|
+
: { name: 'labels:in:issue', status: 'planned', reasonCode: null }
|
|
1501
|
+
: { name: 'labels:in:issue', status: 'skipped', reasonCode: 'TRIAGE_REQUIRED' };
|
|
1317
1502
|
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1503
|
+
const planned = planPullRequestMetadata({
|
|
1504
|
+
pullRequest: inspected.pullRequest,
|
|
1505
|
+
issue: issue.issue,
|
|
1506
|
+
taskType: String(base.frontmatter.type || 'task'),
|
|
1507
|
+
issueNumber: base.issueNumber,
|
|
1508
|
+
capabilities: base.context.capabilities,
|
|
1509
|
+
inLabels
|
|
1510
|
+
}).operations.filter((operation) => options.metadata || operation.name === 'closing-issue')
|
|
1511
|
+
.filter((operation) => options.closingIssue || operation.name !== 'closing-issue');
|
|
1512
|
+
const operations: PlatformOperation[] = [
|
|
1513
|
+
...(issueInLabelOperation ? [issueInLabelOperation] : []),
|
|
1514
|
+
...planned.map(({ name, status, reasonCode }) => ({ name, status, reasonCode }))
|
|
1515
|
+
];
|
|
1516
|
+
const milestoneOperation = planned.find((operation) => operation.name === 'milestone' && operation.status === 'planned');
|
|
1517
|
+
if (milestoneOperation && base.provider.issues?.listMilestones) {
|
|
1518
|
+
const milestones = await base.provider.issues.listMilestones({ context: providerOperationContext(base.loadedContext) });
|
|
1519
|
+
if (!milestones.ok) return providerPullRequestError(base, milestones.error, base.prNumber);
|
|
1520
|
+
}
|
|
1521
|
+
if (options.dryRun) return softenFailure(result(planned.some((operation) => operation.status === 'planned') ? 'planned' : 'no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1522
|
+
platform: base.context.platform, capabilities: base.context.capabilities, pullRequest: inspected.pullRequest, operations, error: null
|
|
1523
|
+
}));
|
|
1524
|
+
let issueChanged = false;
|
|
1525
|
+
if (issueInLabelOperation?.status === 'planned') {
|
|
1526
|
+
const desiredIssueLabels = [...new Set([
|
|
1527
|
+
...issue.issue.labels.filter((label) => !label.startsWith('in:')),
|
|
1528
|
+
...inLabels
|
|
1529
|
+
])].sort();
|
|
1530
|
+
const updatedIssue = base.provider.issues?.update
|
|
1531
|
+
? await base.provider.issues.update({
|
|
1532
|
+
context: providerOperationContext(base.loadedContext),
|
|
1533
|
+
target: base.issueIdentity!,
|
|
1534
|
+
currentLabels: issue.issue.labels,
|
|
1535
|
+
patch: { labels: desiredIssueLabels },
|
|
1536
|
+
mutation: { idempotencyKey: `issue:update:${base.resolved.taskId}:in-labels` }
|
|
1537
|
+
})
|
|
1538
|
+
: unsupportedProviderOperation(base.provider, 'issues.update');
|
|
1539
|
+
if (!updatedIssue.ok) return softenFailure(providerPullRequestError(base, updatedIssue.error, base.prNumber));
|
|
1540
|
+
issueChanged = updatedIssue.value.changed;
|
|
1541
|
+
const rereadIssue = base.provider.issues?.inspect
|
|
1542
|
+
? await base.provider.issues.inspect({ context: providerOperationContext(base.loadedContext), target: base.issueIdentity! })
|
|
1543
|
+
: unsupportedProviderOperation(base.provider, 'issues.inspect');
|
|
1544
|
+
if (!rereadIssue.ok) return softenFailure(providerPullRequestError(base, {
|
|
1545
|
+
...rereadIssue.error,
|
|
1546
|
+
code: 'IN_LABEL_SYNC_PARTIAL',
|
|
1547
|
+
message: `In-label synchronization is partial or unknown: ${rereadIssue.error.message}`
|
|
1548
|
+
}, base.prNumber));
|
|
1549
|
+
const actualIssueLabels = rereadIssue.value.labels.filter((label) => label.startsWith('in:')).sort();
|
|
1550
|
+
if (actualIssueLabels.join('\0') !== inLabels.join('\0')) return softenFailure(providerPullRequestError(base, {
|
|
1551
|
+
code: 'IN_LABEL_SYNC_PARTIAL',
|
|
1552
|
+
message: 'Issue in: labels did not converge after update',
|
|
1553
|
+
retryable: true
|
|
1554
|
+
}, base.prNumber));
|
|
1555
|
+
}
|
|
1556
|
+
const patch: Record<string, unknown> = {};
|
|
1557
|
+
for (const operation of planned) {
|
|
1558
|
+
if (operation.status !== 'planned') continue;
|
|
1559
|
+
if (operation.name === 'labels') patch.labels = operation.value;
|
|
1560
|
+
if (operation.name === 'assignees') patch.assignees = operation.value;
|
|
1561
|
+
if (operation.name === 'closing-issue') patch.body = operation.value;
|
|
1562
|
+
if (operation.name === 'milestone') patch.milestone = operation.value;
|
|
1563
|
+
}
|
|
1564
|
+
if (Object.keys(patch).length === 0) return softenFailure(result(planned.some((operation) => operation.status === 'skipped') ? 'degraded' : 'no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1565
|
+
platform: base.context.platform, capabilities: base.context.capabilities, resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: inspected.pullRequest, operations, error: null
|
|
1566
|
+
}));
|
|
1567
|
+
const updated = base.provider.changeRequests?.update
|
|
1568
|
+
? await base.provider.changeRequests.update({
|
|
1569
|
+
context: providerOperationContext(base.loadedContext),
|
|
1570
|
+
target: base.prIdentity,
|
|
1571
|
+
currentLabels: inspected.pullRequest.labels,
|
|
1572
|
+
patch: patch as never,
|
|
1573
|
+
mutation: { idempotencyKey: `pull-request:update:${base.resolved.taskId}` }
|
|
1574
|
+
})
|
|
1575
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.update');
|
|
1576
|
+
if (!updated.ok) {
|
|
1577
|
+
const error = issueChanged && !updated.error.code.startsWith('IN_LABEL_SYNC')
|
|
1578
|
+
? { ...updated.error, code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${updated.error.message}` }
|
|
1579
|
+
: updated.error;
|
|
1580
|
+
return softenFailure(providerPullRequestError(base, error, base.prNumber));
|
|
1581
|
+
}
|
|
1582
|
+
let finalPullRequest = inspected.pullRequest;
|
|
1583
|
+
if (planned.some((operation) => operation.name === 'labels' && operation.status === 'planned')) {
|
|
1584
|
+
const reread = base.provider.changeRequests?.inspect
|
|
1585
|
+
? await base.provider.changeRequests.inspect({ context: providerOperationContext(base.loadedContext), target: base.prIdentity })
|
|
1586
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
|
|
1587
|
+
if (!reread.ok) return softenFailure(providerPullRequestError(base, {
|
|
1588
|
+
...reread.error,
|
|
1589
|
+
code: 'IN_LABEL_SYNC_PARTIAL',
|
|
1590
|
+
message: `In-label synchronization is partial or unknown: ${reread.error.message}`
|
|
1591
|
+
}, base.prNumber));
|
|
1592
|
+
finalPullRequest = normalizeProviderPullRequest(reread.value, base.context.platform.repository!, base.prNumber || 0, inspected.pullRequest);
|
|
1593
|
+
const expected = ((planned.find((operation) => operation.name === 'labels')?.value as string[] || [])
|
|
1594
|
+
.filter((label) => label.startsWith('in:') || label.startsWith('type:'))).sort();
|
|
1595
|
+
const actual = finalPullRequest.labels.filter((label) => label.startsWith('in:') || label.startsWith('type:')).sort();
|
|
1596
|
+
if (actual.join('\0') !== expected.join('\0')) return softenFailure(providerPullRequestError(base, {
|
|
1597
|
+
code: 'IN_LABEL_SYNC_PARTIAL',
|
|
1598
|
+
message: 'Pull request labels did not converge after update',
|
|
1599
|
+
retryable: true
|
|
1600
|
+
}, base.prNumber));
|
|
1601
|
+
}
|
|
1602
|
+
return softenFailure(result(planned.some((operation) => operation.status === 'skipped') ? 'degraded' : 'applied', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1603
|
+
changed: issueChanged || updated.value.changed,
|
|
1604
|
+
platform: base.context.platform,
|
|
1605
|
+
capabilities: base.context.capabilities,
|
|
1606
|
+
resource: { kind: 'pull-request', number: base.prNumber },
|
|
1607
|
+
pullRequest: finalPullRequest,
|
|
1608
|
+
operations: operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation),
|
|
1609
|
+
error: null
|
|
1323
1610
|
}));
|
|
1324
1611
|
}
|
|
1325
|
-
const patched = base.client.json<RemotePullRequest>(['api', `repos/${base.context.platform.repository}/issues/${base.prNumber}`, '-X', 'PATCH', '--input', '-'], {
|
|
1326
|
-
cwd: base.resolved.repoRoot, method: 'PATCH', input: JSON.stringify(payload)
|
|
1327
|
-
});
|
|
1328
|
-
if (!patched.ok) return softenFailure(result(patched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { pullRequest: fetched.value, operations, error: patched.error }));
|
|
1329
|
-
return softenFailure(result(planned.some((operation) => operation.status === 'skipped') ? 'degraded' : 'applied', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1330
|
-
changed: true, platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1331
|
-
resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: fetched.value,
|
|
1332
|
-
operations: operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation), error: null
|
|
1333
|
-
}));
|
|
1334
1612
|
}
|
|
1335
1613
|
|
|
1336
1614
|
function readProjectPrFlow(repoRoot: string): 'required' | 'disabled' | undefined {
|
|
@@ -1356,11 +1634,13 @@ function skipPlatformPullRequestFactUnlocked(
|
|
|
1356
1634
|
error: { code: 'TASK_DOCUMENT_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
1357
1635
|
});
|
|
1358
1636
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1637
|
+
let issueIdentity: ResourceIdentity | null;
|
|
1638
|
+
try { issueIdentity = taskIssueIdentity(frontmatter, undefined, options.runtimeVersion); }
|
|
1639
|
+
catch (error) { return result('failed', resolved.taskId, null, null, { error: { ...taskIssueIdentityError(error), retryable: false } }); }
|
|
1640
|
+
const issueNumber = resourceIdentityNumber(issueIdentity);
|
|
1641
|
+
const existing = readPrDeliveryFact(frontmatter, options.runtimeVersion);
|
|
1362
1642
|
if (existing.status === 'invalid') return result('failed', resolved.taskId, issueNumber, null, {
|
|
1363
|
-
error: { code:
|
|
1643
|
+
error: { code: existing.error.code, message: existing.error.message, retryable: false }
|
|
1364
1644
|
});
|
|
1365
1645
|
if (existing.status === 'missing') return result('failed', resolved.taskId, issueNumber, null, {
|
|
1366
1646
|
error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
|
|
@@ -1369,9 +1649,12 @@ function skipPlatformPullRequestFactUnlocked(
|
|
|
1369
1649
|
operations: [{ name: 'task:skip-pr', status: 'no-op', reasonCode: null }],
|
|
1370
1650
|
error: null
|
|
1371
1651
|
});
|
|
1372
|
-
if (existing.fact.state === 'bound')
|
|
1373
|
-
|
|
1374
|
-
|
|
1652
|
+
if (existing.fact.state === 'bound') {
|
|
1653
|
+
const boundNumber = resourceIdentityNumber(existing.fact.identity.resource);
|
|
1654
|
+
return result('failed', resolved.taskId, issueNumber, boundNumber, {
|
|
1655
|
+
error: { code: 'PR_DELIVERY_FACT_CONFLICT', message: `Task is already bound to PR ${boundNumber === null ? 'identity' : `#${boundNumber}`}`, retryable: false }
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1375
1658
|
if (readProjectPrFlow(resolved.repoRoot) === 'required') return result('failed', resolved.taskId, issueNumber, null, {
|
|
1376
1659
|
error: { code: 'PR_SKIP_FORBIDDEN', message: 'Cannot skip PR delivery when prFlow is required', retryable: false }
|
|
1377
1660
|
});
|
|
@@ -1400,7 +1683,7 @@ function skipPlatformPullRequestFactUnlocked(
|
|
|
1400
1683
|
});
|
|
1401
1684
|
}
|
|
1402
1685
|
|
|
1403
|
-
function skipPlatformPullRequestFact(taskRef: string, options: SkipFactOptions): PullRequestResult {
|
|
1686
|
+
async function skipPlatformPullRequestFact(taskRef: string, options: SkipFactOptions): Promise<PullRequestResult> {
|
|
1404
1687
|
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
1405
1688
|
if (!resolved.ok) return result('failed', resolved.taskId, null, null, {
|
|
1406
1689
|
error: { code: resolved.code, message: resolved.message, retryable: false }
|
|
@@ -1433,12 +1716,12 @@ export {
|
|
|
1433
1716
|
inspectPlatformPullRequest,
|
|
1434
1717
|
inspectPlatformPullRequestByNumber,
|
|
1435
1718
|
normalizePullRequest,
|
|
1436
|
-
resolveGitHubChangeRequestGitEvidence,
|
|
1437
1719
|
resolveExternalPullRequest,
|
|
1438
1720
|
skipPlatformPullRequestFact,
|
|
1439
1721
|
selectExternalPullRequest,
|
|
1440
1722
|
selectPullRequest,
|
|
1441
|
-
syncPlatformPullRequest
|
|
1723
|
+
syncPlatformPullRequest,
|
|
1724
|
+
syncPlatformPullRequestInLabels
|
|
1442
1725
|
};
|
|
1443
1726
|
export type { BindOptions, CreateOptions, ExternalPullRequestResult, ExternalPullRequestSelection, PullRequestPrimaryResult, PullRequestResult, PullRequestSnapshot, ResolveExternalOptions, SkipFactOptions, SyncOptions };
|
|
1444
1727
|
export { warningResultForPrimary };
|