@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
|
@@ -5,17 +5,21 @@ import { parseTypedTaskFrontmatter } from '../task/frontmatter.js';
|
|
|
5
5
|
import { resolveTaskRef } from '../task/resolve-ref.js';
|
|
6
6
|
import { extractSection } from '../task/sections.js';
|
|
7
7
|
import { captureTaskWriteMetadata, writeTask } from '../task/write.js';
|
|
8
|
-
import {
|
|
9
|
-
import { parseGitHubRemote, resolvePlatformContext } from './context.js';
|
|
8
|
+
import { parseGitHubRemote, resolvePlatformContext, resolvePlatformProviderContext } from './context.js';
|
|
10
9
|
import { createGitHubClient } from './github-client.js';
|
|
11
10
|
import { inspectPlatformIssue } from './issues.js';
|
|
11
|
+
import { inspectGitHubIssue } from './issues.js';
|
|
12
12
|
import { planPullRequestMetadata } from './pull-request-metadata.js';
|
|
13
|
+
import { extractPullRequestFileNames, planInLabelUpdate, syncLabelDelta, validateInLabelMapping, validateRepositoryLabelPayload } from './in-label-sync.js';
|
|
13
14
|
import { platformResult } from './types.js';
|
|
14
15
|
import { inspectCompletionArtifacts } from '../task/finalization-artifacts.js';
|
|
15
16
|
import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.js';
|
|
16
17
|
import { resolveDeliveryTarget } from '../task/delivery-target.js';
|
|
17
18
|
import { mergeOperationWarnings } from '../task/operation-outcome.js';
|
|
18
19
|
import { buildBoundFact, buildSkippedFact, factFrontmatterMutation, readPrDeliveryFact } from '../task/pr-delivery-fact.js';
|
|
20
|
+
import { providerError, providerOperationContext, providerStatus, providerResourceToken, providerResourceIdentity, resourceIdentity, unsupportedProviderOperation } from './provider-bridge.js';
|
|
21
|
+
import { isResourceIdentity, resourceIdentityEquals, resourceIdentityNumber } from './resource-identity.js';
|
|
22
|
+
import { taskIssueIdentity, taskIssueIdentityError } from './task-identities.js';
|
|
19
23
|
function warningResultForPrimary(primaryResult) {
|
|
20
24
|
if (primaryResult === 'pr_created')
|
|
21
25
|
return 'pr_created_with_warnings';
|
|
@@ -105,9 +109,15 @@ function normalizeClosingPullRequest(node, repository) {
|
|
|
105
109
|
function repositoryKey(repository) {
|
|
106
110
|
return repository.trim().toLowerCase();
|
|
107
111
|
}
|
|
112
|
+
function isUsableResourceIdentity(identity) {
|
|
113
|
+
return isResourceIdentity(identity);
|
|
114
|
+
}
|
|
115
|
+
function identityLabel(identity) {
|
|
116
|
+
return identity.kind === 'number' ? `PR #${identity.value}` : `PR ${identity.kind}:${identity.value}`;
|
|
117
|
+
}
|
|
108
118
|
function identitySignature(pullRequest) {
|
|
109
119
|
return [
|
|
110
|
-
pullRequest.repository, pullRequest
|
|
120
|
+
pullRequest.repository, JSON.stringify(pullRequestIdentity(pullRequest)), pullRequest.nodeId, pullRequest.url,
|
|
111
121
|
pullRequest.head.repository, pullRequest.head.ref, pullRequest.head.sha,
|
|
112
122
|
pullRequest.base.repository, pullRequest.base.ref, pullRequest.base.sha,
|
|
113
123
|
pullRequest.mergedAt || '', pullRequest.mergeCommitSha || ''
|
|
@@ -119,48 +129,52 @@ function hasCompleteExternalIdentity(pullRequest) {
|
|
|
119
129
|
pullRequest.head.repository, pullRequest.head.ref, pullRequest.head.sha,
|
|
120
130
|
pullRequest.base.repository, pullRequest.base.ref, pullRequest.base.sha
|
|
121
131
|
];
|
|
122
|
-
return
|
|
123
|
-
required.every((value) => Boolean(value?.trim())) &&
|
|
132
|
+
return isUsableResourceIdentity(pullRequestIdentity(pullRequest)) && required.every((value) => Boolean(value?.trim())) &&
|
|
124
133
|
(pullRequest.state === 'open' || Boolean(pullRequest.mergedAt) === Boolean(pullRequest.mergeCommitSha));
|
|
125
134
|
}
|
|
126
|
-
function selectExternalPullRequest(candidates, repository,
|
|
135
|
+
function selectExternalPullRequest(candidates, repository, existingPrIdentity, explicitPrIdentity) {
|
|
136
|
+
const existingIdentity = typeof existingPrIdentity === 'number'
|
|
137
|
+
? resourceIdentity(existingPrIdentity) : existingPrIdentity;
|
|
138
|
+
const explicitIdentity = typeof explicitPrIdentity === 'number'
|
|
139
|
+
? resourceIdentity(explicitPrIdentity) : explicitPrIdentity;
|
|
127
140
|
const invalid = candidates.find((candidate) => !hasCompleteExternalIdentity(candidate));
|
|
128
141
|
if (invalid)
|
|
129
142
|
return {
|
|
130
143
|
status: 'failed', code: 'PR_IDENTITY_INVALID',
|
|
131
144
|
message: `Closing PR #${invalid.number} lacks required identity`, candidates, eligible: []
|
|
132
145
|
};
|
|
133
|
-
const
|
|
146
|
+
const byIdentity = new Map();
|
|
134
147
|
for (const candidate of candidates) {
|
|
135
148
|
const signature = identitySignature(candidate);
|
|
136
|
-
const
|
|
149
|
+
const identityKey = JSON.stringify(pullRequestIdentity(candidate));
|
|
150
|
+
const prior = byIdentity.get(identityKey);
|
|
137
151
|
if (prior && prior !== signature)
|
|
138
152
|
return {
|
|
139
153
|
status: 'failed', code: 'PR_IDENTITY_INVALID',
|
|
140
154
|
message: `Closing PR #${candidate.number} has conflicting identities`, candidates, eligible: []
|
|
141
155
|
};
|
|
142
|
-
|
|
156
|
+
byIdentity.set(identityKey, signature);
|
|
143
157
|
}
|
|
144
|
-
const unique = candidates.filter((candidate, index) => candidates.findIndex((other) => other
|
|
158
|
+
const unique = candidates.filter((candidate, index) => candidates.findIndex((other) => resourceIdentityEquals(pullRequestIdentity(other), pullRequestIdentity(candidate))) === index);
|
|
145
159
|
const wantedRepository = repositoryKey(repository);
|
|
146
160
|
const eligible = unique.filter((candidate) => repositoryKey(candidate.repository) === wantedRepository &&
|
|
147
161
|
repositoryKey(candidate.base.repository) === wantedRepository &&
|
|
148
162
|
candidate.state === 'closed' &&
|
|
149
163
|
Boolean(candidate.mergedAt && candidate.mergeCommitSha));
|
|
150
|
-
if (
|
|
151
|
-
const selected = unique.find((candidate) => candidate
|
|
164
|
+
if (explicitIdentity !== null) {
|
|
165
|
+
const selected = unique.find((candidate) => resourceIdentityEquals(pullRequestIdentity(candidate), explicitIdentity));
|
|
152
166
|
if (!selected)
|
|
153
|
-
return { status: 'failed', code: 'PR_NOT_FOUND', message: `PR
|
|
154
|
-
if (!eligible.some((candidate) => candidate
|
|
155
|
-
return { status: 'failed', code: 'PR_IDENTITY_INVALID', message: `PR
|
|
167
|
+
return { status: 'failed', code: 'PR_NOT_FOUND', message: `PR ${identityLabel(explicitIdentity)} is not a closing PR`, candidates, eligible };
|
|
168
|
+
if (!eligible.some((candidate) => resourceIdentityEquals(pullRequestIdentity(candidate), explicitIdentity))) {
|
|
169
|
+
return { status: 'failed', code: 'PR_IDENTITY_INVALID', message: `PR ${identityLabel(explicitIdentity)} is not an eligible merged PR`, candidates, eligible };
|
|
156
170
|
}
|
|
157
|
-
if (
|
|
158
|
-
return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to
|
|
171
|
+
if (existingIdentity !== null && !resourceIdentityEquals(existingIdentity, explicitIdentity)) {
|
|
172
|
+
return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to ${identityLabel(existingIdentity)}`, candidates, eligible };
|
|
159
173
|
}
|
|
160
174
|
return { status: 'selected', source: 'explicit', selected, candidates, eligible };
|
|
161
175
|
}
|
|
162
|
-
if (
|
|
163
|
-
return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to
|
|
176
|
+
if (existingIdentity !== null && eligible.length === 1 && !resourceIdentityEquals(existingIdentity, pullRequestIdentity(eligible[0]))) {
|
|
177
|
+
return { status: 'failed', code: 'PR_BIND_CONFLICT', message: `Task is already bound to ${identityLabel(existingIdentity)}`, candidates, eligible };
|
|
164
178
|
}
|
|
165
179
|
if (eligible.length === 0)
|
|
166
180
|
return { status: 'normal', candidates, eligible: [] };
|
|
@@ -238,64 +252,6 @@ function selectPullRequest(remotes, repository, head, base, expectedSha) {
|
|
|
238
252
|
return { status: 'resolved', pullRequest: matches[0] };
|
|
239
253
|
return { status: matches.length === 0 ? 'missing' : 'ambiguous', pullRequest: null };
|
|
240
254
|
}
|
|
241
|
-
function repositoryHead(cwd) {
|
|
242
|
-
return execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
243
|
-
cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']
|
|
244
|
-
}).trim();
|
|
245
|
-
}
|
|
246
|
-
function remoteBranchHead(cwd, repository, head) {
|
|
247
|
-
const wanted = expectedHead(repository, head);
|
|
248
|
-
if (!wanted.ref || !/^[A-Za-z0-9._/-]+$/.test(wanted.ref))
|
|
249
|
-
return {
|
|
250
|
-
ok: false,
|
|
251
|
-
error: { code: 'PR_HEAD_INVALID', message: 'Pull request head ref is invalid', retryable: false }
|
|
252
|
-
};
|
|
253
|
-
const remotes = configuredGitRemotes(cwd);
|
|
254
|
-
const remote = remotes.find((item) => repositoryKey(item.repository) === repositoryKey(wanted.repository))
|
|
255
|
-
?? remotes.find((item) => item.name === 'origin');
|
|
256
|
-
if (!remote)
|
|
257
|
-
return {
|
|
258
|
-
ok: false,
|
|
259
|
-
error: { code: 'PR_REMOTE_BRANCH_MISSING', message: `No configured remote can verify ${wanted.repository}:${wanted.ref}`, retryable: false }
|
|
260
|
-
};
|
|
261
|
-
try {
|
|
262
|
-
const output = execFileSync('git', ['ls-remote', '--refs', remote.name, `refs/heads/${wanted.ref}`], {
|
|
263
|
-
cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']
|
|
264
|
-
}).trim();
|
|
265
|
-
const match = output.split(/\r?\n/).map((line) => line.trim()).find((line) => line.endsWith(`refs/heads/${wanted.ref}`));
|
|
266
|
-
const sha = match?.split(/\s+/)[0] || null;
|
|
267
|
-
if (!sha || !/^[a-f0-9]{40}$/i.test(sha))
|
|
268
|
-
return {
|
|
269
|
-
ok: false,
|
|
270
|
-
error: { code: 'PR_REMOTE_BRANCH_MISSING', message: `Remote branch ${wanted.repository}:${wanted.ref} does not exist`, retryable: false }
|
|
271
|
-
};
|
|
272
|
-
return { ok: true, value: sha };
|
|
273
|
-
}
|
|
274
|
-
catch (error) {
|
|
275
|
-
return {
|
|
276
|
-
ok: false,
|
|
277
|
-
error: { code: 'PR_REMOTE_BRANCH_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: true }
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
function verifyCreateHead(base, head) {
|
|
282
|
-
let localHead;
|
|
283
|
-
try {
|
|
284
|
-
localHead = repositoryHead(base.resolved.repoRoot);
|
|
285
|
-
}
|
|
286
|
-
catch (error) {
|
|
287
|
-
return { ok: false, error: { code: 'PR_LOCAL_HEAD_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: false } };
|
|
288
|
-
}
|
|
289
|
-
const remote = remoteBranchHead(base.resolved.repoRoot, base.context.platform.repository, head);
|
|
290
|
-
if (!remote.ok)
|
|
291
|
-
return remote;
|
|
292
|
-
if (remote.value !== localHead)
|
|
293
|
-
return {
|
|
294
|
-
ok: false,
|
|
295
|
-
error: { code: 'PR_REMOTE_HEAD_MISMATCH', message: `Remote head ${remote.value} does not match local HEAD ${localHead}`, retryable: false }
|
|
296
|
-
};
|
|
297
|
-
return { ok: true, value: localHead };
|
|
298
|
-
}
|
|
299
255
|
function validateBoundPullRequest(base, pullRequest, head, baseRef, expectedSha) {
|
|
300
256
|
if (!pullRequest)
|
|
301
257
|
return {
|
|
@@ -376,7 +332,7 @@ function validateExternalMergedEvidence(initial, rechecked) {
|
|
|
376
332
|
};
|
|
377
333
|
return { ok: true };
|
|
378
334
|
}
|
|
379
|
-
function resolvedContext(taskRef, options) {
|
|
335
|
+
async function resolvedContext(taskRef, options) {
|
|
380
336
|
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
381
337
|
if (!resolved.ok)
|
|
382
338
|
return { ok: false, output: result('failed', resolved.taskId, null, null, {
|
|
@@ -384,24 +340,87 @@ function resolvedContext(taskRef, options) {
|
|
|
384
340
|
}) };
|
|
385
341
|
const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
386
342
|
const frontmatter = parseTypedTaskFrontmatter(content);
|
|
387
|
-
const factRead = readPrDeliveryFact(frontmatter);
|
|
343
|
+
const factRead = readPrDeliveryFact(frontmatter, options.runtimeVersion);
|
|
388
344
|
if (factRead.status === 'invalid')
|
|
389
345
|
return { ok: false, output: result('failed', resolved.taskId, null, null, {
|
|
390
|
-
error: { code:
|
|
346
|
+
error: { code: factRead.error.code, message: factRead.error.message, retryable: false }
|
|
391
347
|
}) };
|
|
392
348
|
const fact = factRead.status === 'valid' ? factRead.fact : null;
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
349
|
+
let issueIdentity;
|
|
350
|
+
try {
|
|
351
|
+
issueIdentity = taskIssueIdentity(frontmatter, undefined, options.runtimeVersion);
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
return { ok: false, output: result('failed', resolved.taskId, null, null, { error: { ...taskIssueIdentityError(error), retryable: false } }) };
|
|
355
|
+
}
|
|
356
|
+
const issueNumber = resourceIdentityNumber(issueIdentity);
|
|
357
|
+
const prIdentity = fact?.state === 'bound' ? fact.identity.resource : null;
|
|
358
|
+
const prNumber = resourceIdentityNumber(prIdentity);
|
|
359
|
+
const client = options.client;
|
|
360
|
+
const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client });
|
|
361
|
+
const context = loaded.ok ? loaded.value.context : loaded.context;
|
|
399
362
|
const usable = (context.status === 'no-op' || context.status === 'degraded') && context.platform.repository;
|
|
400
|
-
if (!usable)
|
|
363
|
+
if (!usable || !loaded.ok)
|
|
401
364
|
return { ok: false, output: result(context.status, resolved.taskId, issueNumber, prNumber, {
|
|
402
365
|
platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
|
|
403
366
|
}) };
|
|
404
|
-
return { ok: true, resolved, content, frontmatter, fact, issueNumber, prNumber, client, context };
|
|
367
|
+
return { ok: true, resolved, content, frontmatter, fact, issueIdentity, issueNumber, prIdentity, prNumber, client, context, provider: loaded.value.provider, providerType: loaded.value.providerType, loadedContext: loaded.value };
|
|
368
|
+
}
|
|
369
|
+
function normalizeProviderPullRequest(remote, repository, fallbackNumber, fallback) {
|
|
370
|
+
const head = remote.head || fallback?.head || { repository, ref: '', sha: remote.headSha || '' };
|
|
371
|
+
const base = remote.base || fallback?.base || { repository, ref: '', sha: remote.baseSha || '' };
|
|
372
|
+
const identity = remote.identity || fallback?.identity || (remote.number ? { kind: 'number', value: remote.number } : { kind: 'id', value: remote.id });
|
|
373
|
+
const snapshot = {
|
|
374
|
+
repository,
|
|
375
|
+
number: remote.number ?? resourceIdentityNumber(identity) ?? fallbackNumber,
|
|
376
|
+
nodeId: remote.id,
|
|
377
|
+
url: remote.displayUrl || fallback?.url || '',
|
|
378
|
+
state: remote.state === 'closed' ? 'closed' : 'open',
|
|
379
|
+
title: remote.title,
|
|
380
|
+
body: remote.body,
|
|
381
|
+
draft: remote.draft ?? fallback?.draft ?? false,
|
|
382
|
+
head: { ...head, sha: remote.headSha || head.sha },
|
|
383
|
+
base: { ...base, sha: remote.baseSha || base.sha },
|
|
384
|
+
mergedAt: remote.mergedAt ?? fallback?.mergedAt ?? null,
|
|
385
|
+
mergeCommitSha: remote.mergeCommitSha ?? fallback?.mergeCommitSha ?? null,
|
|
386
|
+
labels: [...(remote.labels || fallback?.labels || [])].sort(),
|
|
387
|
+
assignees: [...(remote.assignees || fallback?.assignees || [])].sort(),
|
|
388
|
+
milestone: remote.milestone ?? fallback?.milestone ?? null,
|
|
389
|
+
mergeability: remote.mergeability || fallback?.mergeability || { state: 'unknown', detail: null }
|
|
390
|
+
};
|
|
391
|
+
Object.defineProperty(snapshot, 'identity', { value: identity, enumerable: false, configurable: true });
|
|
392
|
+
return snapshot;
|
|
393
|
+
}
|
|
394
|
+
function pullRequestIdentity(pullRequest) {
|
|
395
|
+
return pullRequest.identity || { kind: 'number', value: pullRequest.number };
|
|
396
|
+
}
|
|
397
|
+
function providerPullRequestError(base, error, prNumber) {
|
|
398
|
+
return result(error.code.startsWith('IN_LABEL_SYNC') ? 'blocked' : providerStatus(error), base.resolved.taskId, base.issueNumber, prNumber, {
|
|
399
|
+
changed: error.code.startsWith('IN_LABEL_SYNC'),
|
|
400
|
+
platform: base.context.platform,
|
|
401
|
+
capabilities: base.context.capabilities,
|
|
402
|
+
resource: { kind: 'pull-request', number: prNumber },
|
|
403
|
+
error: providerError(error, 'PLATFORM_PROVIDER_OPERATION_FAILED')
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
async function inspectExternalPullRequest(base, prIdentity) {
|
|
407
|
+
const prNumber = resourceIdentityNumber(prIdentity);
|
|
408
|
+
const fallback = base.fact?.state === 'bound' && resourceIdentityEquals(base.fact.identity.resource, prIdentity)
|
|
409
|
+
? base.fact.identity
|
|
410
|
+
: null;
|
|
411
|
+
const inspected = base.provider.changeRequests?.inspect
|
|
412
|
+
? await base.provider.changeRequests.inspect({ context: providerOperationContext(base.loadedContext), target: prIdentity })
|
|
413
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
|
|
414
|
+
if (!inspected.ok)
|
|
415
|
+
return providerPullRequestError(base, inspected.error, prNumber);
|
|
416
|
+
const pullRequest = normalizeProviderPullRequest(inspected.value, base.context.platform.repository, prNumber || 0, fallback);
|
|
417
|
+
return result('no-op', base.resolved.taskId, base.issueNumber, prNumber, {
|
|
418
|
+
platform: base.context.platform,
|
|
419
|
+
capabilities: base.context.capabilities,
|
|
420
|
+
resource: { kind: 'pull-request', number: prNumber, identity: prIdentity },
|
|
421
|
+
pullRequest,
|
|
422
|
+
error: null
|
|
423
|
+
});
|
|
405
424
|
}
|
|
406
425
|
function inspectGitHubPullRequest(client, repository, number, cwd) {
|
|
407
426
|
const fetched = client.json(['api', `repos/${repository}/pulls/${number}`], { cwd });
|
|
@@ -412,189 +431,340 @@ function inspectGitHubPullRequest(client, repository, number, cwd) {
|
|
|
412
431
|
? { ok: true, value: pullRequest }
|
|
413
432
|
: { ok: false, error: { code: 'PR_IDENTITY_INVALID', message: 'Remote resource is not a valid pull request', retryable: false } };
|
|
414
433
|
}
|
|
415
|
-
function
|
|
434
|
+
async function inspectPlatformPullRequest(taskRef, options = {}) {
|
|
435
|
+
const base = await resolvedContext(taskRef, options);
|
|
436
|
+
if (!base.ok)
|
|
437
|
+
return base.output;
|
|
438
|
+
if (!base.fact)
|
|
439
|
+
return result('no-op', base.resolved.taskId, base.issueNumber, null, {
|
|
440
|
+
error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
|
|
441
|
+
});
|
|
442
|
+
if (!base.prIdentity)
|
|
443
|
+
return result('no-op', base.resolved.taskId, base.issueNumber, null, {
|
|
444
|
+
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
445
|
+
error: { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false }
|
|
446
|
+
});
|
|
447
|
+
return inspectExternalPullRequest(base, base.prIdentity);
|
|
448
|
+
}
|
|
449
|
+
async function inspectPlatformPullRequestByNumber(prNumber, options = {}) {
|
|
450
|
+
const loaded = await resolvePlatformProviderContext({ cwd: options.cwd || process.cwd(), client: options.client });
|
|
451
|
+
const context = loaded.ok ? loaded.value.context : loaded.context;
|
|
452
|
+
const usable = (context.status === 'no-op' || context.status === 'degraded') && context.platform.repository;
|
|
453
|
+
if (!usable) {
|
|
454
|
+
return result(context.status, null, null, null, {
|
|
455
|
+
platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
let identity;
|
|
416
459
|
try {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}).trim();
|
|
424
|
-
const repository = parseGitHubRemote(url);
|
|
425
|
-
return repository ? [{ name, url, repository }] : [];
|
|
426
|
-
}
|
|
427
|
-
catch {
|
|
428
|
-
return [];
|
|
429
|
-
}
|
|
460
|
+
identity = loaded.ok ? providerResourceToken(loaded.value.provider, 'pull-request', String(prNumber)) : resourceIdentity(prNumber);
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
return result('failed', null, null, null, {
|
|
464
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
465
|
+
error: { code: 'PLATFORM_IDENTITY_TOKEN_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
430
466
|
});
|
|
431
467
|
}
|
|
432
|
-
|
|
433
|
-
|
|
468
|
+
if (loaded.ok) {
|
|
469
|
+
const inspected = loaded.value.provider.changeRequests?.inspect
|
|
470
|
+
? await loaded.value.provider.changeRequests.inspect({ context: providerOperationContext(loaded.value), target: identity })
|
|
471
|
+
: unsupportedProviderOperation(loaded.value.provider, 'changeRequests.inspect');
|
|
472
|
+
const number = resourceIdentityNumber(identity);
|
|
473
|
+
if (!inspected.ok)
|
|
474
|
+
return result(providerStatus(inspected.error), null, null, number, {
|
|
475
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
476
|
+
resource: { kind: 'pull-request', number, identity }, error: providerError(inspected.error, 'PLATFORM_PROVIDER_OPERATION_FAILED')
|
|
477
|
+
});
|
|
478
|
+
return result('no-op', null, null, number, {
|
|
479
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
480
|
+
resource: { kind: 'pull-request', number, identity },
|
|
481
|
+
pullRequest: normalizeProviderPullRequest(inspected.value, context.platform.repository, number || 0), error: null
|
|
482
|
+
});
|
|
434
483
|
}
|
|
484
|
+
return result('failed', null, null, null, {
|
|
485
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
486
|
+
resource: { kind: 'pull-request', number: null },
|
|
487
|
+
error: { code: 'PLATFORM_PROVIDER_LOAD_FAILED', message: 'Selected provider failed to load', retryable: false }
|
|
488
|
+
});
|
|
435
489
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
490
|
+
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}}}}}';
|
|
491
|
+
function readInLabelMapping(repoRoot) {
|
|
492
|
+
try {
|
|
493
|
+
const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
|
|
494
|
+
const mapping = validateInLabelMapping(config.labels?.in);
|
|
495
|
+
return mapping.ok ? { ok: true, value: mapping.value } : mapping;
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
return {
|
|
499
|
+
ok: false,
|
|
500
|
+
error: { code: 'IN_LABEL_SYNC_CONFIG_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
501
|
+
};
|
|
502
|
+
}
|
|
439
503
|
}
|
|
440
|
-
function
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
pullRequest.number <= 0) {
|
|
504
|
+
function taskInLabelTarget(repoRoot, frontmatter, currentLabels, repositoryLabels) {
|
|
505
|
+
const baseRef = typeof frontmatter.delivery_base_ref === 'string' ? frontmatter.delivery_base_ref.trim() : '';
|
|
506
|
+
if (!baseRef)
|
|
444
507
|
return {
|
|
445
508
|
ok: false,
|
|
446
|
-
error: {
|
|
447
|
-
code: 'PR_MERGE_EVIDENCE_SOURCE_UNAVAILABLE',
|
|
448
|
-
message: 'Pull request repository identity is inconsistent',
|
|
449
|
-
retryable: false
|
|
450
|
-
}
|
|
509
|
+
error: { code: 'IN_LABEL_SYNC_BASE_MISSING', message: 'Task has no delivery_base_ref for in-label evidence', retryable: false }
|
|
451
510
|
};
|
|
511
|
+
let changedFiles;
|
|
512
|
+
try {
|
|
513
|
+
changedFiles = execFileSync('git', ['diff', `${baseRef}...HEAD`, '--name-only'], {
|
|
514
|
+
cwd: repoRoot, encoding: 'utf8'
|
|
515
|
+
}).trim().split(/\r?\n/).filter(Boolean);
|
|
452
516
|
}
|
|
453
|
-
|
|
454
|
-
const targetHeadRef = `refs/heads/${pullRequest.base.ref}`;
|
|
455
|
-
const refValid = (ref) => {
|
|
456
|
-
try {
|
|
457
|
-
execFileSync('git', ['check-ref-format', ref], {
|
|
458
|
-
cwd, encoding: 'utf8', stdio: ['ignore', 'ignore', 'ignore']
|
|
459
|
-
});
|
|
460
|
-
return true;
|
|
461
|
-
}
|
|
462
|
-
catch {
|
|
463
|
-
return false;
|
|
464
|
-
}
|
|
465
|
-
};
|
|
466
|
-
if (!refValid(reviewedHeadRef) || !refValid(targetHeadRef)) {
|
|
517
|
+
catch (error) {
|
|
467
518
|
return {
|
|
468
519
|
ok: false,
|
|
469
|
-
error: {
|
|
470
|
-
code: 'PR_MERGE_EVIDENCE_SOURCE_UNAVAILABLE',
|
|
471
|
-
message: 'Pull request Git refs are invalid',
|
|
472
|
-
retryable: false
|
|
473
|
-
}
|
|
520
|
+
error: { code: 'IN_LABEL_SYNC_EVIDENCE_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
474
521
|
};
|
|
475
522
|
}
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
523
|
+
const mapping = readInLabelMapping(repoRoot);
|
|
524
|
+
if (!mapping.ok)
|
|
525
|
+
return mapping;
|
|
526
|
+
const planned = planInLabelUpdate({ changedFiles, currentLabels, mapping: mapping.value, repositoryLabels });
|
|
527
|
+
if (planned.error)
|
|
528
|
+
return { ok: false, error: planned.error };
|
|
529
|
+
return { ok: true, value: planned.target };
|
|
530
|
+
}
|
|
531
|
+
function inspectClosingIssueNumbers(client, repository, pullRequest, cwd) {
|
|
532
|
+
const [owner, name] = repository.split('/');
|
|
533
|
+
if (!owner || !name)
|
|
484
534
|
return {
|
|
485
535
|
ok: false,
|
|
486
|
-
error: {
|
|
487
|
-
code: 'PR_MERGE_EVIDENCE_SOURCE_UNAVAILABLE',
|
|
488
|
-
message: `No GitHub remote can provide evidence for ${repository}`,
|
|
489
|
-
retryable: false
|
|
490
|
-
}
|
|
536
|
+
error: { code: 'IN_LABEL_SYNC_REPOSITORY_INVALID', message: 'Repository identity is invalid', retryable: false }
|
|
491
537
|
};
|
|
538
|
+
let cursor = null;
|
|
539
|
+
const numbers = [];
|
|
540
|
+
for (;;) {
|
|
541
|
+
const args = ['api', 'graphql', '-f', `query=${CLOSING_ISSUES_QUERY}`, '-F', `owner=${owner}`, '-F', `name=${name}`, '-F', `number=${pullRequest}`];
|
|
542
|
+
if (cursor)
|
|
543
|
+
args.push('-F', `cursor=${cursor}`);
|
|
544
|
+
const response = client.json(args, { cwd });
|
|
545
|
+
if (!response.ok)
|
|
546
|
+
return response;
|
|
547
|
+
const connection = response.value?.data?.repository?.pullRequest?.closingIssuesReferences;
|
|
548
|
+
if (!connection || !Array.isArray(connection.nodes) || !connection.pageInfo)
|
|
549
|
+
return {
|
|
550
|
+
ok: false,
|
|
551
|
+
error: { code: 'IN_LABEL_SYNC_ASSOCIATION_INVALID', message: 'Closing Issue response is incomplete', retryable: false }
|
|
552
|
+
};
|
|
553
|
+
for (const node of connection.nodes) {
|
|
554
|
+
const issue = Number(node?.number);
|
|
555
|
+
if (!Number.isSafeInteger(issue) || issue <= 0)
|
|
556
|
+
return {
|
|
557
|
+
ok: false,
|
|
558
|
+
error: { code: 'IN_LABEL_SYNC_ASSOCIATION_INVALID', message: 'Closing Issue identity is invalid', retryable: false }
|
|
559
|
+
};
|
|
560
|
+
numbers.push(issue);
|
|
561
|
+
}
|
|
562
|
+
if (!connection.pageInfo.hasNextPage)
|
|
563
|
+
return { ok: true, value: [...new Set(numbers)] };
|
|
564
|
+
if (!connection.pageInfo.endCursor || connection.pageInfo.endCursor === cursor)
|
|
565
|
+
return {
|
|
566
|
+
ok: false,
|
|
567
|
+
error: { code: 'IN_LABEL_SYNC_ASSOCIATION_INVALID', message: 'Closing Issue pagination cursor is invalid', retryable: false }
|
|
568
|
+
};
|
|
569
|
+
cursor = connection.pageInfo.endCursor;
|
|
492
570
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
571
|
+
}
|
|
572
|
+
function inspectRepositoryLabels(client, repository, cwd) {
|
|
573
|
+
const response = client.json(['api', '--paginate', '--slurp', `repos/${repository}/labels?per_page=100`], { cwd });
|
|
574
|
+
if (!response.ok)
|
|
575
|
+
return response;
|
|
576
|
+
const labels = validateRepositoryLabelPayload(response.value);
|
|
577
|
+
return labels.ok ? { ok: true, value: new Set(labels.value) } : labels;
|
|
578
|
+
}
|
|
579
|
+
function inspectPullRequestFiles(client, repository, pullRequest, cwd) {
|
|
580
|
+
const response = client.json(['api', '--paginate', '--slurp', `repos/${repository}/pulls/${pullRequest}/files?per_page=100`], { cwd });
|
|
581
|
+
if (!response.ok)
|
|
582
|
+
return response;
|
|
583
|
+
const files = extractPullRequestFileNames(response.value);
|
|
584
|
+
return files ? { ok: true, value: files } : {
|
|
585
|
+
ok: false,
|
|
586
|
+
error: { code: 'IN_LABEL_SYNC_FILES_INVALID', message: 'Pull request files response is incomplete', retryable: false }
|
|
496
587
|
};
|
|
497
588
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
589
|
+
function inspectTaskInLabelTarget(repoRoot, frontmatter, pullRequest, repository, client) {
|
|
590
|
+
const baseRef = typeof frontmatter.delivery_base_ref === 'string' ? frontmatter.delivery_base_ref.trim() : '';
|
|
591
|
+
if (!baseRef)
|
|
592
|
+
return {
|
|
593
|
+
ok: false,
|
|
594
|
+
error: { code: 'IN_LABEL_SYNC_BASE_MISSING', message: 'Task has no delivery_base_ref for in-label evidence', retryable: false }
|
|
595
|
+
};
|
|
596
|
+
let changedFiles;
|
|
597
|
+
try {
|
|
598
|
+
changedFiles = execFileSync('git', ['diff', `${baseRef}...HEAD`, '--name-only'], {
|
|
599
|
+
cwd: repoRoot, encoding: 'utf8'
|
|
600
|
+
}).trim().split(/\r?\n/).filter(Boolean);
|
|
507
601
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
'api', `repos/${repository}/pulls?state=open&base=${encodeURIComponent(target)}&per_page=100`
|
|
513
|
-
], { cwd: base.resolved.repoRoot });
|
|
514
|
-
if (!listed.ok)
|
|
515
|
-
return listed;
|
|
516
|
-
const selected = selectPullRequest(listed.value, repository, head, target, expectedSha);
|
|
517
|
-
return selected.status === 'resolved'
|
|
518
|
-
? { ok: true, value: selected.pullRequest }
|
|
519
|
-
: { ok: false, error: {
|
|
520
|
-
code: selected.status === 'ambiguous' ? 'PR_IDENTITY_AMBIGUOUS'
|
|
521
|
-
: selected.status === 'head-mismatch' ? 'PR_HEAD_SHA_MISMATCH' : 'PR_NOT_FOUND',
|
|
522
|
-
message: selected.status === 'ambiguous' ? 'Multiple pull requests match the exact head/base identity'
|
|
523
|
-
: selected.status === 'head-mismatch' ? 'A pull request matches head/base but not the expected head SHA'
|
|
524
|
-
: 'No pull request matches the exact head/base identity',
|
|
525
|
-
retryable: false
|
|
526
|
-
} };
|
|
527
|
-
}
|
|
528
|
-
function inspectPlatformPullRequest(taskRef, options = {}) {
|
|
529
|
-
const base = resolvedContext(taskRef, options);
|
|
530
|
-
if (!base.ok)
|
|
531
|
-
return base.output;
|
|
532
|
-
if (!base.fact)
|
|
533
|
-
return result('no-op', base.resolved.taskId, base.issueNumber, null, {
|
|
534
|
-
error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
|
|
535
|
-
});
|
|
536
|
-
if (!base.prNumber)
|
|
537
|
-
return result('no-op', base.resolved.taskId, base.issueNumber, null, {
|
|
538
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
539
|
-
error: { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false }
|
|
540
|
-
});
|
|
541
|
-
const fetched = inspectPlatformChangeRequest(base.context.platform.type, {
|
|
542
|
-
cwd: base.resolved.repoRoot,
|
|
543
|
-
repository: base.context.platform.repository,
|
|
544
|
-
number: base.prNumber,
|
|
545
|
-
client: options.client || base.client
|
|
546
|
-
});
|
|
547
|
-
if (!fetched.ok || !fetched.value) {
|
|
548
|
-
const error = fetched.error || {
|
|
549
|
-
code: 'PR_INSPECTION_INVALID',
|
|
550
|
-
message: 'Platform adapter returned no change-request snapshot',
|
|
551
|
-
retryable: false
|
|
602
|
+
catch (error) {
|
|
603
|
+
return {
|
|
604
|
+
ok: false,
|
|
605
|
+
error: { code: 'IN_LABEL_SYNC_EVIDENCE_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
552
606
|
};
|
|
553
|
-
return result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
554
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
555
|
-
resource: { kind: 'pull-request', number: base.prNumber }, error
|
|
556
|
-
});
|
|
557
607
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
608
|
+
const mapping = readInLabelMapping(repoRoot);
|
|
609
|
+
if (!mapping.ok)
|
|
610
|
+
return mapping;
|
|
611
|
+
const labels = inspectRepositoryLabels(client, repository, repoRoot);
|
|
612
|
+
if (!labels.ok)
|
|
613
|
+
return labels;
|
|
614
|
+
const planned = planInLabelUpdate({
|
|
615
|
+
changedFiles, currentLabels: pullRequest.labels, mapping: mapping.value, repositoryLabels: labels.value
|
|
561
616
|
});
|
|
617
|
+
if (planned.error)
|
|
618
|
+
return { ok: false, error: planned.error };
|
|
619
|
+
return { ok: true, value: { changedFiles, target: planned.target } };
|
|
620
|
+
}
|
|
621
|
+
function inLabelResource(kind, number, before, expected, after, effect) {
|
|
622
|
+
return { kind, number, before: [...before].sort(), expected: [...expected].sort(), after: after ? [...after].sort() : null, effect };
|
|
562
623
|
}
|
|
563
|
-
function
|
|
624
|
+
async function syncPlatformPullRequestInLabels(prNumber, options = {}) {
|
|
625
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
564
626
|
const client = options.client || createGitHubClient();
|
|
565
|
-
const context = resolvePlatformContext({ cwd
|
|
627
|
+
const context = await resolvePlatformContext({ cwd, client });
|
|
566
628
|
const usable = (context.status === 'no-op' || context.status === 'degraded') && context.platform.repository;
|
|
567
|
-
if (!usable)
|
|
568
|
-
return result(context.status, null, null,
|
|
629
|
+
if (!usable)
|
|
630
|
+
return result(context.status, null, null, prNumber, {
|
|
569
631
|
platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
|
|
570
632
|
});
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
return result('failed', null, null, null, {
|
|
633
|
+
if (!Number.isSafeInteger(prNumber) || prNumber <= 0)
|
|
634
|
+
return result('failed', null, null, prNumber, {
|
|
574
635
|
platform: context.platform, capabilities: context.capabilities,
|
|
575
636
|
error: { code: 'PR_NUMBER_INVALID', message: 'PR number must be positive', retryable: false }
|
|
576
637
|
});
|
|
577
|
-
|
|
578
|
-
const fetched =
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
638
|
+
const repository = context.platform.repository;
|
|
639
|
+
const fetched = inspectGitHubPullRequest(client, repository, prNumber, cwd);
|
|
640
|
+
if (!fetched.ok)
|
|
641
|
+
return result(fetched.error.retryable ? 'blocked' : 'failed', null, null, prNumber, {
|
|
642
|
+
platform: context.platform, capabilities: context.capabilities, resource: { kind: 'pull-request', number: prNumber }, error: fetched.error
|
|
643
|
+
});
|
|
644
|
+
const files = inspectPullRequestFiles(client, repository, prNumber, cwd);
|
|
645
|
+
if (!files.ok)
|
|
646
|
+
return result(files.error.retryable ? 'blocked' : 'failed', null, null, prNumber, {
|
|
647
|
+
platform: context.platform, capabilities: context.capabilities, pullRequest: fetched.value,
|
|
648
|
+
resource: { kind: 'pull-request', number: prNumber }, error: files.error
|
|
649
|
+
});
|
|
650
|
+
const mapping = readInLabelMapping(cwd);
|
|
651
|
+
if (!mapping.ok)
|
|
652
|
+
return result('failed', null, null, prNumber, {
|
|
653
|
+
platform: context.platform, capabilities: context.capabilities, pullRequest: fetched.value,
|
|
654
|
+
resource: { kind: 'pull-request', number: prNumber }, error: mapping.error
|
|
655
|
+
});
|
|
656
|
+
const repositoryLabels = inspectRepositoryLabels(client, repository, cwd);
|
|
657
|
+
if (!repositoryLabels.ok)
|
|
658
|
+
return result(repositoryLabels.error.retryable ? 'blocked' : 'failed', null, null, prNumber, {
|
|
659
|
+
platform: context.platform, capabilities: context.capabilities, pullRequest: fetched.value,
|
|
660
|
+
resource: { kind: 'pull-request', number: prNumber }, error: repositoryLabels.error
|
|
661
|
+
});
|
|
662
|
+
const plannedTarget = planInLabelUpdate({
|
|
663
|
+
changedFiles: files.value, currentLabels: fetched.value.labels,
|
|
664
|
+
mapping: mapping.value, repositoryLabels: repositoryLabels.value
|
|
583
665
|
});
|
|
584
|
-
if (
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
retryable: false
|
|
589
|
-
};
|
|
590
|
-
return result(error.retryable ? 'blocked' : 'failed', null, null, prNumber, {
|
|
591
|
-
platform: context.platform, capabilities: context.capabilities,
|
|
592
|
-
resource: { kind: 'pull-request', number: prNumber }, error
|
|
666
|
+
if (plannedTarget.error)
|
|
667
|
+
return result('failed', null, null, prNumber, {
|
|
668
|
+
platform: context.platform, capabilities: context.capabilities, pullRequest: fetched.value,
|
|
669
|
+
resource: { kind: 'pull-request', number: prNumber }, error: plannedTarget.error
|
|
593
670
|
});
|
|
671
|
+
const target = plannedTarget.target;
|
|
672
|
+
const association = inspectClosingIssueNumbers(client, repository, prNumber, cwd);
|
|
673
|
+
if (!association.ok)
|
|
674
|
+
return result(association.error.retryable ? 'blocked' : 'failed', null, null, prNumber, {
|
|
675
|
+
platform: context.platform, capabilities: context.capabilities, pullRequest: fetched.value,
|
|
676
|
+
resource: { kind: 'pull-request', number: prNumber }, evidence: { kind: 'pull-request-files', pullRequestFiles: files.value }, error: association.error
|
|
677
|
+
});
|
|
678
|
+
const issueNumber = association.value.length === 1 ? association.value[0] : null;
|
|
679
|
+
let issue = null;
|
|
680
|
+
if (issueNumber) {
|
|
681
|
+
const fetchedIssue = inspectGitHubIssue(client, repository, issueNumber, cwd);
|
|
682
|
+
if (!fetchedIssue.ok)
|
|
683
|
+
return result(fetchedIssue.error.retryable ? 'blocked' : 'failed', null, issueNumber, prNumber, {
|
|
684
|
+
platform: context.platform, capabilities: context.capabilities, pullRequest: fetched.value,
|
|
685
|
+
resource: { kind: 'issue', number: issueNumber }, evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: association.value }, error: fetchedIssue.error
|
|
686
|
+
});
|
|
687
|
+
issue = fetchedIssue.value;
|
|
594
688
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
689
|
+
const resources = issue
|
|
690
|
+
? [
|
|
691
|
+
{ kind: 'issue', number: issue.number, snapshot: issue },
|
|
692
|
+
{ kind: 'pull-request', number: prNumber, snapshot: fetched.value }
|
|
693
|
+
]
|
|
694
|
+
: [{ kind: 'pull-request', number: prNumber, snapshot: fetched.value }];
|
|
695
|
+
const plans = resources.map((resource) => ({
|
|
696
|
+
...resource,
|
|
697
|
+
plan: planInLabelUpdate({
|
|
698
|
+
changedFiles: files.value, currentLabels: resource.snapshot.labels,
|
|
699
|
+
mapping: mapping.value, repositoryLabels: repositoryLabels.value
|
|
700
|
+
})
|
|
701
|
+
}));
|
|
702
|
+
const operations = [
|
|
703
|
+
{ name: 'closing-issues', status: 'no-op', reasonCode: association.value.length === 1 ? null : association.value.length === 0 ? 'ISSUE_ASSOCIATION_NONE' : 'ISSUE_ASSOCIATION_AMBIGUOUS' },
|
|
704
|
+
...plans.map((item) => ({
|
|
705
|
+
name: `labels:in:${item.kind}`,
|
|
706
|
+
status: !context.capabilities.triage ? 'skipped' : item.plan.changed ? 'planned' : 'no-op',
|
|
707
|
+
reasonCode: !context.capabilities.triage ? 'TRIAGE_REQUIRED' : null
|
|
708
|
+
}))
|
|
709
|
+
];
|
|
710
|
+
const willWrite = context.capabilities.triage && !options.dryRun;
|
|
711
|
+
const resourcesState = plans.map((item) => inLabelResource(item.kind, item.number, item.snapshot.labels, target, item.plan.changed && willWrite ? null : item.snapshot.labels, item.plan.changed && willWrite ? 'unknown' : 'no-op'));
|
|
712
|
+
if (options.dryRun || !context.capabilities.triage)
|
|
713
|
+
return result(!context.capabilities.triage || association.value.length !== 1 ? 'degraded' : plans.some((item) => item.plan.changed) ? 'planned' : 'no-op', null, issueNumber, prNumber, {
|
|
714
|
+
platform: context.platform, capabilities: context.capabilities, pullRequest: fetched.value,
|
|
715
|
+
resource: { kind: 'pull-request', number: prNumber }, operations, resources: resourcesState,
|
|
716
|
+
evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: association.value }, error: null
|
|
717
|
+
});
|
|
718
|
+
let currentPullRequest = fetched.value;
|
|
719
|
+
let successfulWrites = 0;
|
|
720
|
+
for (const item of plans) {
|
|
721
|
+
if (!item.plan.changed)
|
|
722
|
+
continue;
|
|
723
|
+
const synced = syncLabelDelta(client, repository, item.number, cwd, item.snapshot.labels, item.plan.target);
|
|
724
|
+
if (synced.status === 'failed' || synced.status === 'blocked') {
|
|
725
|
+
const state = resourcesState.find((resource) => resource.kind === item.kind && resource.number === item.number);
|
|
726
|
+
state.effect = synced.status === 'blocked' ? 'unknown' : 'no-op';
|
|
727
|
+
state.after = synced.status === 'blocked' ? null : item.snapshot.labels;
|
|
728
|
+
const partial = successfulWrites > 0 || synced.status === 'blocked';
|
|
729
|
+
const error = partial && synced.error
|
|
730
|
+
? { ...synced.error, code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${synced.error.message}` }
|
|
731
|
+
: synced.error;
|
|
732
|
+
return result(partial ? 'blocked' : synced.status, null, issueNumber, prNumber, {
|
|
733
|
+
changed: successfulWrites > 0 || synced.changed, platform: context.platform, capabilities: context.capabilities,
|
|
734
|
+
pullRequest: currentPullRequest, resource: { kind: 'pull-request', number: prNumber }, operations, resources: resourcesState,
|
|
735
|
+
evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: association.value }, error
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
successfulWrites += synced.changed ? 1 : 0;
|
|
739
|
+
const reread = item.kind === 'issue'
|
|
740
|
+
? inspectGitHubIssue(client, repository, item.number, cwd)
|
|
741
|
+
: inspectGitHubPullRequest(client, repository, item.number, cwd);
|
|
742
|
+
if (!reread.ok)
|
|
743
|
+
return result('blocked', null, issueNumber, prNumber, {
|
|
744
|
+
changed: true, platform: context.platform, capabilities: context.capabilities,
|
|
745
|
+
pullRequest: currentPullRequest, resource: { kind: 'pull-request', number: prNumber }, operations, resources: resourcesState,
|
|
746
|
+
evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: association.value },
|
|
747
|
+
error: { ...reread.error, code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${reread.error.message}` }
|
|
748
|
+
});
|
|
749
|
+
const after = reread.value.labels;
|
|
750
|
+
const state = resourcesState.find((resource) => resource.kind === item.kind && resource.number === item.number);
|
|
751
|
+
state.after = after;
|
|
752
|
+
state.effect = after.filter((label) => label.startsWith('in:')).sort().join('\0') === target.join('\0') ? 'applied' : 'unknown';
|
|
753
|
+
if (state.effect === 'unknown')
|
|
754
|
+
return result('blocked', null, issueNumber, prNumber, {
|
|
755
|
+
changed: true, platform: context.platform, capabilities: context.capabilities,
|
|
756
|
+
pullRequest: currentPullRequest, resource: { kind: 'pull-request', number: prNumber }, operations, resources: resourcesState,
|
|
757
|
+
evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: association.value },
|
|
758
|
+
error: { code: 'IN_LABEL_SYNC_PARTIAL', message: `${item.kind} #${item.number} did not converge to the expected in: labels`, retryable: true }
|
|
759
|
+
});
|
|
760
|
+
if (item.kind === 'pull-request')
|
|
761
|
+
currentPullRequest = reread.value;
|
|
762
|
+
}
|
|
763
|
+
return result(association.value.length !== 1 ? 'degraded' : successfulWrites > 0 ? 'applied' : 'no-op', null, issueNumber, prNumber, {
|
|
764
|
+
changed: successfulWrites > 0, platform: context.platform, capabilities: context.capabilities,
|
|
765
|
+
pullRequest: currentPullRequest, resource: { kind: 'pull-request', number: prNumber },
|
|
766
|
+
operations: operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation),
|
|
767
|
+
resources: resourcesState, evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: association.value }, error: null
|
|
598
768
|
});
|
|
599
769
|
}
|
|
600
770
|
function appendActivity(content, line) {
|
|
@@ -603,9 +773,8 @@ function appendActivity(content, line) {
|
|
|
603
773
|
}
|
|
604
774
|
function deliveryIdentity(pullRequest) {
|
|
605
775
|
return {
|
|
776
|
+
resource: pullRequestIdentity(pullRequest),
|
|
606
777
|
repository: pullRequest.repository,
|
|
607
|
-
number: pullRequest.number,
|
|
608
|
-
nodeId: pullRequest.nodeId,
|
|
609
778
|
url: pullRequest.url,
|
|
610
779
|
head: { ...pullRequest.head },
|
|
611
780
|
base: { ...pullRequest.base }
|
|
@@ -614,11 +783,11 @@ function deliveryIdentity(pullRequest) {
|
|
|
614
783
|
function factTimestamp(timestamp) {
|
|
615
784
|
return new Date(timestamp.replace(' ', 'T')).toISOString();
|
|
616
785
|
}
|
|
617
|
-
function boundFactFor(pullRequest, source,
|
|
786
|
+
function boundFactFor(pullRequest, source, issue, verifiedAt) {
|
|
618
787
|
return buildBoundFact({
|
|
619
788
|
identity: deliveryIdentity(pullRequest),
|
|
620
789
|
source,
|
|
621
|
-
issueNumber,
|
|
790
|
+
...(typeof issue === 'object' ? { issueIdentity: issue } : { issueNumber: issue }),
|
|
622
791
|
verifiedAt: factTimestamp(verifiedAt),
|
|
623
792
|
remoteState: pullRequest.state,
|
|
624
793
|
mergedAt: pullRequest.state === 'closed' ? pullRequest.mergedAt : null,
|
|
@@ -645,11 +814,11 @@ function bindIdentity(base, pullRequest, agent, source, dryRun = false, expected
|
|
|
645
814
|
});
|
|
646
815
|
if (!identity.ok)
|
|
647
816
|
return { status: 'failed', error: identity.error };
|
|
648
|
-
if (base.
|
|
817
|
+
if (base.prIdentity && !resourceIdentityEquals(base.prIdentity, pullRequestIdentity(pullRequest)))
|
|
649
818
|
return { status: 'failed', error: {
|
|
650
|
-
code: 'PR_BIND_CONFLICT', message:
|
|
819
|
+
code: 'PR_BIND_CONFLICT', message: 'Task is already bound to a different pull request identity'
|
|
651
820
|
} };
|
|
652
|
-
if (base.
|
|
821
|
+
if (base.prIdentity && resourceIdentityEquals(base.prIdentity, pullRequestIdentity(pullRequest)) && base.fact?.state === 'bound') {
|
|
653
822
|
if (JSON.stringify(base.fact.identity) !== JSON.stringify(deliveryIdentity(pullRequest)))
|
|
654
823
|
return { status: 'failed', error: {
|
|
655
824
|
code: 'PR_BIND_IDENTITY_MISMATCH', message: `Task binding for PR #${pullRequest.number} does not match the fetched pull request identity`
|
|
@@ -658,7 +827,7 @@ function bindIdentity(base, pullRequest, agent, source, dryRun = false, expected
|
|
|
658
827
|
}
|
|
659
828
|
const current = fs.readFileSync(base.resolved.taskMdPath, 'utf8');
|
|
660
829
|
const metadata = captureTaskWriteMetadata();
|
|
661
|
-
const fact = boundFactFor(pullRequest, source, base.
|
|
830
|
+
const fact = boundFactFor(pullRequest, source, taskIssueIdentity(base.frontmatter), metadata.timestamp);
|
|
662
831
|
const factMutation = factFrontmatterMutation(fact);
|
|
663
832
|
return writeTask({
|
|
664
833
|
taskRef: base.resolved.taskId,
|
|
@@ -670,64 +839,60 @@ function bindIdentity(base, pullRequest, agent, source, dryRun = false, expected
|
|
|
670
839
|
]
|
|
671
840
|
}, { repoRoot: base.resolved.repoRoot, metadataProvider: () => metadata });
|
|
672
841
|
}
|
|
673
|
-
function bindPlatformPullRequest(taskRef, options) {
|
|
674
|
-
const base = resolvedContext(taskRef, options);
|
|
842
|
+
async function bindPlatformPullRequest(taskRef, options) {
|
|
843
|
+
const base = await resolvedContext(taskRef, options);
|
|
675
844
|
if (!base.ok)
|
|
676
845
|
return base.output;
|
|
677
846
|
if (!base.fact)
|
|
678
847
|
return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
679
848
|
error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
|
|
680
849
|
});
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
850
|
+
let prIdentity;
|
|
851
|
+
try {
|
|
852
|
+
prIdentity = providerResourceToken(base.provider, 'pull-request', String(options.pr));
|
|
853
|
+
}
|
|
854
|
+
catch (error) {
|
|
855
|
+
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 } });
|
|
856
|
+
}
|
|
857
|
+
const requestedNumber = resourceIdentityNumber(prIdentity);
|
|
858
|
+
if (base.prIdentity && !resourceIdentityEquals(base.prIdentity, prIdentity))
|
|
686
859
|
return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
687
|
-
error: { code: 'PR_BIND_CONFLICT', message:
|
|
860
|
+
error: { code: 'PR_BIND_CONFLICT', message: 'Task is already bound to a different pull request identity', retryable: false }
|
|
688
861
|
});
|
|
689
|
-
const
|
|
690
|
-
if (!
|
|
691
|
-
return
|
|
692
|
-
const initialIdentity = validateWriterIdentity(base,
|
|
862
|
+
const inspected = await inspectExternalPullRequest(base, prIdentity);
|
|
863
|
+
if (inspected.status === 'failed' || inspected.status === 'blocked' || !inspected.pullRequest)
|
|
864
|
+
return inspected;
|
|
865
|
+
const initialIdentity = validateWriterIdentity(base, inspected.pullRequest, {
|
|
693
866
|
expectedHead: String(base.frontmatter.branch || ''),
|
|
694
867
|
errorCode: 'PR_BIND_IDENTITY_MISMATCH'
|
|
695
868
|
});
|
|
696
869
|
if (!initialIdentity.ok)
|
|
697
|
-
return result('failed', base.resolved.taskId, base.issueNumber,
|
|
698
|
-
pullRequest:
|
|
699
|
-
});
|
|
700
|
-
const rechecked = inspectGitHubPullRequest(base.client, base.context.platform.repository, options.pr, base.resolved.repoRoot);
|
|
701
|
-
if (!rechecked.ok || !rechecked.value)
|
|
702
|
-
return result(rechecked.ok ? 'failed' : rechecked.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
703
|
-
pullRequest: fetched.value,
|
|
704
|
-
error: rechecked.ok ? { code: 'PR_BIND_RECHECK_FAILED', message: 'Pull request recheck returned no identity', retryable: false } : rechecked.error
|
|
705
|
-
});
|
|
706
|
-
if (!sameDeliveryIdentity(fetched.value, rechecked.value))
|
|
707
|
-
return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
708
|
-
pullRequest: rechecked.value,
|
|
709
|
-
error: { code: 'PR_BIND_IDENTITY_MISMATCH', message: 'Pull request identity changed before task binding', retryable: false }
|
|
870
|
+
return result('failed', base.resolved.taskId, base.issueNumber, requestedNumber, {
|
|
871
|
+
pullRequest: inspected.pullRequest, error: initialIdentity.error
|
|
710
872
|
});
|
|
711
|
-
const written = bindIdentity(base,
|
|
873
|
+
const written = bindIdentity(base, inspected.pullRequest, options.agent, 'explicit-bind', options.dryRun);
|
|
712
874
|
if (written.status === 'failed')
|
|
713
|
-
return result('failed', base.resolved.taskId, base.issueNumber,
|
|
714
|
-
pullRequest:
|
|
875
|
+
return result('failed', base.resolved.taskId, base.issueNumber, requestedNumber, {
|
|
876
|
+
pullRequest: inspected.pullRequest, error: { code: written.error.code, message: written.error.message, retryable: false }
|
|
715
877
|
});
|
|
716
|
-
return result(options.dryRun ? 'planned' : written.status === 'no-op' ? 'no-op' : 'applied', base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber :
|
|
878
|
+
return result(options.dryRun ? 'planned' : written.status === 'no-op' ? 'no-op' : 'applied', base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber : inspected.pullRequest.number, {
|
|
717
879
|
changed: !options.dryRun && written.status === 'applied', platform: base.context.platform, capabilities: base.context.capabilities,
|
|
718
|
-
resource: { kind: 'pull-request', number:
|
|
880
|
+
resource: { kind: 'pull-request', number: inspected.pullRequest.number, identity: pullRequestIdentity(inspected.pullRequest) }, pullRequest: inspected.pullRequest,
|
|
719
881
|
operations: [{ name: 'task:bind-pr', status: options.dryRun ? 'planned' : written.status === 'no-op' ? 'no-op' : 'applied', reasonCode: null }], error: null
|
|
720
882
|
});
|
|
721
883
|
}
|
|
722
|
-
function externalEvidenceNote(
|
|
884
|
+
function externalEvidenceNote(issueIdentity, source, pullRequest) {
|
|
723
885
|
return [
|
|
724
886
|
`authorization=${source}`,
|
|
725
|
-
externalIdentityEvidenceNote(
|
|
887
|
+
externalIdentityEvidenceNote(issueIdentity, pullRequest)
|
|
726
888
|
].join('; ');
|
|
727
889
|
}
|
|
728
|
-
function externalIdentityEvidenceNote(
|
|
890
|
+
function externalIdentityEvidenceNote(issueIdentity, pullRequest) {
|
|
891
|
+
const issueLabel = issueIdentity
|
|
892
|
+
? issueIdentity.kind === 'number' ? `#${issueIdentity.value}` : `${issueIdentity.kind}:${issueIdentity.value}`
|
|
893
|
+
: 'unknown';
|
|
729
894
|
return [
|
|
730
|
-
`issue
|
|
895
|
+
`issue=${issueLabel}`,
|
|
731
896
|
`pr=#${pullRequest.number}`,
|
|
732
897
|
`url=${pullRequest.url}`,
|
|
733
898
|
`base=${pullRequest.base.repository}:${pullRequest.base.ref}@${pullRequest.base.sha}`,
|
|
@@ -736,8 +901,8 @@ function externalIdentityEvidenceNote(issueNumber, pullRequest) {
|
|
|
736
901
|
`mergeCommitSha=${pullRequest.mergeCommitSha}`
|
|
737
902
|
].join('; ');
|
|
738
903
|
}
|
|
739
|
-
function resolveExternalPullRequest(taskRef, options) {
|
|
740
|
-
const base = resolvedContext(taskRef, options);
|
|
904
|
+
async function resolveExternalPullRequest(taskRef, options) {
|
|
905
|
+
const base = await resolvedContext(taskRef, options);
|
|
741
906
|
if (!base.ok)
|
|
742
907
|
return externalResult(base.output);
|
|
743
908
|
if (!base.fact)
|
|
@@ -765,25 +930,40 @@ function resolveExternalPullRequest(taskRef, options) {
|
|
|
765
930
|
error: null
|
|
766
931
|
}), { mode: 'normal' });
|
|
767
932
|
}
|
|
768
|
-
if (!base.
|
|
933
|
+
if (!base.issueIdentity)
|
|
769
934
|
return externalResult(result('failed', base.resolved.taskId, null, base.prNumber, {
|
|
770
935
|
platform: base.context.platform,
|
|
771
936
|
capabilities: base.context.capabilities,
|
|
772
937
|
error: { code: 'EXTERNAL_DELIVERY_ISSUE_REQUIRED', message: 'External delivery requires a valid issue_number', retryable: false }
|
|
773
938
|
}));
|
|
774
|
-
const inspected =
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
939
|
+
const inspected = base.provider.changeRequests?.listClosing
|
|
940
|
+
? await base.provider.changeRequests.listClosing({
|
|
941
|
+
context: providerOperationContext(base.loadedContext),
|
|
942
|
+
issue: base.issueIdentity
|
|
943
|
+
})
|
|
944
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.listClosing');
|
|
780
945
|
if (!inspected.ok || !inspected.value) {
|
|
781
|
-
const error =
|
|
946
|
+
const error = 'error' in inspected && inspected.error
|
|
947
|
+
? inspected.error
|
|
948
|
+
: { code: 'PR_IDENTITY_INVALID', message: 'Closing PR inspection returned no candidates', retryable: false };
|
|
782
949
|
return externalResult(result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
783
950
|
platform: base.context.platform, capabilities: base.context.capabilities, error
|
|
784
951
|
}));
|
|
785
952
|
}
|
|
786
|
-
const
|
|
953
|
+
const candidates = inspected.value.map((item) => normalizeProviderPullRequest(item, base.context.platform.repository, item.number || 0));
|
|
954
|
+
let explicitPrIdentity = null;
|
|
955
|
+
if (options.pr !== undefined) {
|
|
956
|
+
try {
|
|
957
|
+
explicitPrIdentity = providerResourceToken(base.provider, 'pull-request', String(options.pr));
|
|
958
|
+
}
|
|
959
|
+
catch (error) {
|
|
960
|
+
return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
961
|
+
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
962
|
+
error: { code: 'PLATFORM_IDENTITY_TOKEN_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
963
|
+
}));
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
const selected = selectExternalPullRequest(candidates, base.context.platform.repository, base.prIdentity, explicitPrIdentity);
|
|
787
967
|
if (selected.status === 'normal')
|
|
788
968
|
return externalResult(result('no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
789
969
|
platform: base.context.platform,
|
|
@@ -803,7 +983,14 @@ function resolveExternalPullRequest(taskRef, options) {
|
|
|
803
983
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
804
984
|
pullRequest: selected.selected, error: initialIdentity.error
|
|
805
985
|
}), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
|
|
806
|
-
const rechecked =
|
|
986
|
+
const rechecked = base.provider.changeRequests?.inspect
|
|
987
|
+
? await base.provider.changeRequests.inspect({
|
|
988
|
+
context: providerOperationContext(base.loadedContext),
|
|
989
|
+
target: pullRequestIdentity(selected.selected)
|
|
990
|
+
}).then((response) => response.ok
|
|
991
|
+
? { ok: true, value: normalizeProviderPullRequest(response.value, base.context.platform.repository, selected.selected.number, selected.selected) }
|
|
992
|
+
: response)
|
|
993
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
|
|
807
994
|
if (!rechecked.ok || !rechecked.value)
|
|
808
995
|
return externalResult(result(rechecked.ok ? 'failed' : rechecked.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
809
996
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
@@ -829,8 +1016,8 @@ function resolveExternalPullRequest(taskRef, options) {
|
|
|
829
1016
|
pullRequest: rechecked.value, error: finalEvidence.error
|
|
830
1017
|
}), { candidates: selected.candidates, eligible: selected.eligible, selected: rechecked.value });
|
|
831
1018
|
const selectedPullRequest = rechecked.value;
|
|
832
|
-
const note = externalEvidenceNote(base.
|
|
833
|
-
const identityNote = externalIdentityEvidenceNote(base.
|
|
1019
|
+
const note = externalEvidenceNote(base.issueIdentity, selected.source, selectedPullRequest);
|
|
1020
|
+
const identityNote = externalIdentityEvidenceNote(base.issueIdentity, selectedPullRequest);
|
|
834
1021
|
const activityEntry = `**Bind External PR** by ${options.agent} — ${note}`;
|
|
835
1022
|
const auditLines = base.content.split(/\r?\n/).filter((line) => line.includes('**Bind External PR**'));
|
|
836
1023
|
const alreadyAudited = auditLines.some((line) => line.endsWith(identityNote));
|
|
@@ -841,14 +1028,14 @@ function resolveExternalPullRequest(taskRef, options) {
|
|
|
841
1028
|
pullRequest: selectedPullRequest,
|
|
842
1029
|
error: { code: 'PR_BIND_CONFLICT', message: 'Existing external PR audit evidence conflicts with the selected identity', retryable: false }
|
|
843
1030
|
}), { candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
|
|
844
|
-
if (alreadyAudited && base.
|
|
1031
|
+
if (alreadyAudited && base.prIdentity && !resourceIdentityEquals(base.prIdentity, pullRequestIdentity(selectedPullRequest)))
|
|
845
1032
|
return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
846
1033
|
platform: base.context.platform,
|
|
847
1034
|
capabilities: base.context.capabilities,
|
|
848
1035
|
pullRequest: selectedPullRequest,
|
|
849
1036
|
error: { code: 'PR_BIND_CONFLICT', message: 'External PR audit evidence does not match the task binding', retryable: false }
|
|
850
1037
|
}), { candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
|
|
851
|
-
if (base.
|
|
1038
|
+
if (base.prIdentity && resourceIdentityEquals(base.prIdentity, pullRequestIdentity(selectedPullRequest)) && alreadyAudited) {
|
|
852
1039
|
return externalResult(result('no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
853
1040
|
platform: base.context.platform,
|
|
854
1041
|
capabilities: base.context.capabilities,
|
|
@@ -886,18 +1073,42 @@ function resolveExternalPullRequest(taskRef, options) {
|
|
|
886
1073
|
error: null
|
|
887
1074
|
}), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
|
|
888
1075
|
}
|
|
889
|
-
function
|
|
890
|
-
const base = resolvedContext(taskRef, options);
|
|
1076
|
+
async function createPlatformPullRequest(taskRef, options) {
|
|
1077
|
+
const base = await resolvedContext(taskRef, options);
|
|
891
1078
|
if (!base.ok)
|
|
892
1079
|
return withCreation(base.output, PRECONDITION_NOT_CREATED);
|
|
1080
|
+
if (!options.base || !options.head || !options.title.trim() || !options.body.trim()) {
|
|
1081
|
+
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1082
|
+
error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
|
|
1083
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1084
|
+
}
|
|
1085
|
+
try {
|
|
1086
|
+
return withTaskExecutionLock(base.resolved.repoRoot, base.resolved.taskId, 'platform-pr.create', () => createExternalPullRequest(taskRef, options, base));
|
|
1087
|
+
}
|
|
1088
|
+
catch (error) {
|
|
1089
|
+
if (error instanceof TaskExecutionLockError) {
|
|
1090
|
+
return withCreation(result('blocked', base.resolved.taskId, base.issueNumber, null, {
|
|
1091
|
+
platform: base.context.platform,
|
|
1092
|
+
capabilities: base.context.capabilities,
|
|
1093
|
+
error: { code: error.code, message: error.message, retryable: true }
|
|
1094
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1095
|
+
}
|
|
1096
|
+
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1097
|
+
platform: base.context.platform,
|
|
1098
|
+
capabilities: base.context.capabilities,
|
|
1099
|
+
error: {
|
|
1100
|
+
code: 'PR_CREATE_FAILED',
|
|
1101
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1102
|
+
retryable: false
|
|
1103
|
+
}
|
|
1104
|
+
}), PRECONDITION_NOT_CREATED);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
async function createExternalPullRequest(taskRef, options, base) {
|
|
893
1108
|
if (!base.fact)
|
|
894
1109
|
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
895
1110
|
error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
|
|
896
1111
|
}), PRECONDITION_NOT_CREATED);
|
|
897
|
-
if (!options.base || !options.head || !options.title.trim() || !options.body.trim())
|
|
898
|
-
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
899
|
-
error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
|
|
900
|
-
}), PRECONDITION_NOT_CREATED);
|
|
901
1112
|
const target = taskDeliveryTarget(base);
|
|
902
1113
|
if (!target.ok)
|
|
903
1114
|
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
@@ -905,194 +1116,132 @@ function createPlatformPullRequestUnlocked(taskRef, options) {
|
|
|
905
1116
|
}), PRECONDITION_NOT_CREATED);
|
|
906
1117
|
if (options.base !== target.value.baseRef)
|
|
907
1118
|
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
908
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
909
|
-
error: { code: 'PR_DELIVERY_TARGET_MISMATCH', message: `Pull request base '${options.base}' does not match delivery target '${target.value.baseRef}'`, retryable: false }
|
|
910
|
-
}), PRECONDITION_NOT_CREATED);
|
|
911
|
-
if (base.prNumber) {
|
|
912
|
-
const headCheck = verifyCreateHead(base, options.head);
|
|
913
|
-
if (!headCheck.ok)
|
|
914
|
-
return withCreation(result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
915
|
-
platform: base.context.platform,
|
|
916
|
-
capabilities: base.context.capabilities,
|
|
917
|
-
error: headCheck.error
|
|
918
|
-
}), PRECONDITION_NOT_CREATED);
|
|
919
|
-
const inspected = inspectPlatformPullRequest(taskRef, options);
|
|
920
|
-
const identity = validateBoundPullRequest(base, inspected.pullRequest, options.head, options.base, headCheck.value);
|
|
921
|
-
if (!identity.ok)
|
|
922
|
-
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
923
|
-
platform: base.context.platform,
|
|
924
|
-
capabilities: base.context.capabilities,
|
|
925
|
-
pullRequest: inspected.pullRequest,
|
|
926
|
-
error: identity.error
|
|
927
|
-
}), PRECONDITION_NOT_CREATED);
|
|
928
|
-
return withCreation({ ...inspected, result: 'no_op' }, { kind: 'no-op', createdByCurrentOperation: false });
|
|
929
|
-
}
|
|
930
|
-
const headCheck = verifyCreateHead(base, options.head);
|
|
931
|
-
if (!headCheck.ok)
|
|
932
|
-
return withCreation(result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, null, {
|
|
933
1119
|
platform: base.context.platform,
|
|
934
1120
|
capabilities: base.context.capabilities,
|
|
935
|
-
error:
|
|
936
|
-
}), PRECONDITION_NOT_CREATED);
|
|
937
|
-
const expectedHeadSha = headCheck.value;
|
|
938
|
-
const located = locatePullRequest(base, options.head, options.base, expectedHeadSha);
|
|
939
|
-
if (!located.ok && located.error.code === 'PR_IDENTITY_AMBIGUOUS')
|
|
940
|
-
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
941
|
-
platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
|
|
942
|
-
}), PRECONDITION_NOT_CREATED);
|
|
943
|
-
if (!located.ok && located.error.code === 'PR_HEAD_SHA_MISMATCH')
|
|
944
|
-
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
945
|
-
platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
|
|
946
|
-
}), PRECONDITION_NOT_CREATED);
|
|
947
|
-
if (!located.ok && located.error.code !== 'PR_NOT_FOUND')
|
|
948
|
-
return withCreation(result(located.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, null, {
|
|
949
|
-
platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
|
|
1121
|
+
error: { code: 'PR_DELIVERY_TARGET_MISMATCH', message: `Pull request base '${options.base}' does not match delivery target '${target.value.baseRef}'`, retryable: false }
|
|
950
1122
|
}), PRECONDITION_NOT_CREATED);
|
|
1123
|
+
let verifiedHeadSha;
|
|
1124
|
+
if (base.provider.changeRequests?.verifyHead) {
|
|
1125
|
+
const verified = await base.provider.changeRequests.verifyHead({
|
|
1126
|
+
context: providerOperationContext(base.loadedContext),
|
|
1127
|
+
head: options.head
|
|
1128
|
+
});
|
|
1129
|
+
if (!verified.ok)
|
|
1130
|
+
return withCreation(providerPullRequestError(base, verified.error, base.prNumber), PRECONDITION_NOT_CREATED);
|
|
1131
|
+
verifiedHeadSha = verified.value.sha;
|
|
1132
|
+
}
|
|
1133
|
+
if (base.prIdentity) {
|
|
1134
|
+
const inspected = await inspectExternalPullRequest(base, base.prIdentity);
|
|
1135
|
+
return inspected.status === 'failed' || inspected.status === 'blocked'
|
|
1136
|
+
? withCreation(inspected, PRECONDITION_NOT_CREATED)
|
|
1137
|
+
: withCreation({ ...inspected, result: 'no_op' }, { kind: 'no-op', createdByCurrentOperation: false });
|
|
1138
|
+
}
|
|
951
1139
|
if (options.dryRun)
|
|
952
1140
|
return withCreation(result('planned', base.resolved.taskId, base.issueNumber, null, {
|
|
953
|
-
platform: base.context.platform,
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1141
|
+
platform: base.context.platform,
|
|
1142
|
+
capabilities: base.context.capabilities,
|
|
1143
|
+
operations: [{ name: 'pr:create', status: 'planned', reasonCode: null }],
|
|
1144
|
+
result: 'pr_created',
|
|
1145
|
+
error: null
|
|
1146
|
+
}), { kind: 'planned', action: 'create', createdByCurrentOperation: false });
|
|
957
1147
|
const started = writeCreateStarted(base, options.agent);
|
|
958
1148
|
if (started.status === 'failed')
|
|
959
1149
|
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
960
1150
|
error: { code: started.error.code, message: started.error.message, retryable: false }
|
|
961
1151
|
}), PRECONDITION_NOT_CREATED);
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
options.
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
options.phase.value = 'post-accepted';
|
|
980
|
-
pullRequest = normalizePullRequest(response.value, repository);
|
|
981
|
-
}
|
|
982
|
-
else if (response.error.retryable) {
|
|
983
|
-
return withCreation(result('blocked', base.resolved.taskId, base.issueNumber, null, {
|
|
984
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
985
|
-
operations: [{ name: 'pr:create', status: 'failed', reasonCode: 'PR_CREATE_OUTCOME_UNKNOWN' }],
|
|
986
|
-
error: { code: 'PR_CREATE_OUTCOME_UNKNOWN', message: response.error.message, retryable: true }
|
|
987
|
-
}), { kind: 'unknown', errorCode: 'PR_CREATE_OUTCOME_UNKNOWN' });
|
|
988
|
-
}
|
|
989
|
-
else
|
|
990
|
-
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
991
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
992
|
-
operations: [{ name: 'pr:create', status: 'failed', reasonCode: response.error.code }], error: response.error
|
|
993
|
-
}), { kind: 'not-created', reason: 'post-rejected', createdByCurrentOperation: false });
|
|
994
|
-
if (!pullRequest)
|
|
995
|
-
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
996
|
-
error: { code: 'PR_CREATE_RESPONSE_INVALID', message: 'PR create response lacks validated identity', retryable: false }
|
|
997
|
-
}), { kind: 'created', createdByCurrentOperation: true });
|
|
998
|
-
if (pullRequest.head.sha !== expectedHeadSha)
|
|
999
|
-
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1000
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1001
|
-
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
|
|
1002
|
-
error: { code: 'PR_HEAD_SHA_MISMATCH', message: 'Created pull request head does not match the expected local HEAD', retryable: false }
|
|
1003
|
-
}), { kind: 'created', createdByCurrentOperation: true });
|
|
1004
|
-
}
|
|
1005
|
-
const bindHeadCheck = verifyCreateHead(base, options.head);
|
|
1006
|
-
if (!bindHeadCheck.ok || bindHeadCheck.value !== expectedHeadSha) {
|
|
1007
|
-
const error = bindHeadCheck.ok
|
|
1008
|
-
? { code: 'PR_REMOTE_HEAD_MISMATCH', message: 'Remote head changed before task binding', retryable: false }
|
|
1009
|
-
: bindHeadCheck.error;
|
|
1010
|
-
return withCreation(result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1011
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1012
|
-
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest, error
|
|
1013
|
-
}), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false });
|
|
1152
|
+
const created = base.provider.changeRequests?.create
|
|
1153
|
+
? await base.provider.changeRequests.create({
|
|
1154
|
+
context: providerOperationContext(base.loadedContext),
|
|
1155
|
+
base: options.base,
|
|
1156
|
+
head: options.head,
|
|
1157
|
+
title: options.title,
|
|
1158
|
+
body: options.body,
|
|
1159
|
+
draft: Boolean(options.draft),
|
|
1160
|
+
mutation: { idempotencyKey: `pull-request:create:${base.resolved.taskId}` }
|
|
1161
|
+
})
|
|
1162
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.create');
|
|
1163
|
+
if (!created.ok)
|
|
1164
|
+
return withCreation(providerPullRequestError(base, created.error, null), { kind: 'unknown', errorCode: 'PR_CREATE_OUTCOME_UNKNOWN' });
|
|
1165
|
+
const remoteId = String(created.value.remoteId || '');
|
|
1166
|
+
let createdIdentity;
|
|
1167
|
+
try {
|
|
1168
|
+
createdIdentity = providerResourceIdentity(base.provider, 'pull-request', remoteId);
|
|
1014
1169
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
const error = beforeBind.ok
|
|
1018
|
-
? { code: 'PR_BIND_RECHECK_FAILED', message: 'Pull request recheck returned no identity', retryable: false }
|
|
1019
|
-
: beforeBind.error;
|
|
1020
|
-
return withCreation(result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1170
|
+
catch (error) {
|
|
1171
|
+
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1021
1172
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1022
|
-
|
|
1023
|
-
}),
|
|
1173
|
+
error: { code: 'PR_CREATE_RESPONSE_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
1174
|
+
}), { kind: 'created', createdByCurrentOperation: true });
|
|
1024
1175
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1176
|
+
const number = resourceIdentityNumber(createdIdentity);
|
|
1177
|
+
const fallbackNumber = number ?? 0;
|
|
1178
|
+
const fallback = normalizeProviderPullRequest({
|
|
1179
|
+
id: remoteId,
|
|
1180
|
+
identity: createdIdentity,
|
|
1181
|
+
number: number || undefined,
|
|
1182
|
+
state: 'open',
|
|
1183
|
+
title: options.title,
|
|
1184
|
+
body: options.body,
|
|
1185
|
+
headSha: '',
|
|
1186
|
+
baseSha: '',
|
|
1187
|
+
head: { repository: base.context.platform.repository, ref: options.head, sha: '' },
|
|
1188
|
+
base: { repository: base.context.platform.repository, ref: options.base, sha: '' },
|
|
1189
|
+
displayUrl: `${base.provider.type}:${remoteId}`
|
|
1190
|
+
}, base.context.platform.repository, fallbackNumber);
|
|
1191
|
+
const inspected = base.provider.changeRequests?.inspect
|
|
1192
|
+
? await base.provider.changeRequests.inspect({
|
|
1193
|
+
context: providerOperationContext(base.loadedContext),
|
|
1194
|
+
target: createdIdentity
|
|
1195
|
+
})
|
|
1196
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
|
|
1197
|
+
if (!inspected.ok)
|
|
1198
|
+
return withCreation(providerPullRequestError(base, inspected.error, number), { kind: 'created', createdByCurrentOperation: true });
|
|
1199
|
+
const pullRequest = normalizeProviderPullRequest(inspected.value, base.context.platform.repository, inspected.value.number ?? fallback.number, fallback);
|
|
1200
|
+
if (!pullRequest.nodeId || !isUsableResourceIdentity(pullRequestIdentity(pullRequest)))
|
|
1037
1201
|
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1202
|
+
platform: base.context.platform,
|
|
1203
|
+
capabilities: base.context.capabilities,
|
|
1204
|
+
error: { code: 'PR_CREATE_RESPONSE_INVALID', message: 'Provider create response lacks validated identity', retryable: false }
|
|
1205
|
+
}), { kind: 'created', createdByCurrentOperation: true });
|
|
1206
|
+
const identity = validateWriterIdentity(base, pullRequest, {
|
|
1207
|
+
expectedHead: options.head,
|
|
1208
|
+
expectedHeadSha: verifiedHeadSha,
|
|
1209
|
+
errorCode: 'PR_BIND_IDENTITY_MISMATCH'
|
|
1210
|
+
});
|
|
1211
|
+
if (!identity.ok)
|
|
1212
|
+
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1213
|
+
platform: base.context.platform,
|
|
1214
|
+
capabilities: base.context.capabilities,
|
|
1215
|
+
resource: { kind: 'pull-request', number: pullRequest.number },
|
|
1216
|
+
pullRequest,
|
|
1217
|
+
error: identity.error
|
|
1218
|
+
}), { kind: 'created', createdByCurrentOperation: true });
|
|
1219
|
+
const bound = bindIdentity(base, pullRequest, options.agent, 'created', false, options.head);
|
|
1041
1220
|
if (bound.status === 'failed')
|
|
1042
|
-
return withCreation(result('failed', base.resolved.taskId, base.issueNumber,
|
|
1043
|
-
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1044
|
-
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
|
|
1045
|
-
operations: [{ name: created ? 'pr:create' : 'pr:reuse', status: 'applied', reasonCode: null }, { name: 'task:bind-pr', status: 'failed', reasonCode: bound.error.code }],
|
|
1046
|
-
error: { code: 'PR_CREATED_BIND_FAILED', message: `${pullRequest.url}: ${bound.error.message}`, retryable: false }
|
|
1047
|
-
}), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'reused', createdByCurrentOperation: false });
|
|
1048
|
-
return withCreation(result('applied', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1049
|
-
changed: true, platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1050
|
-
resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
|
|
1051
|
-
operations: [{ name: created ? 'pr:create' : 'pr:reuse', status: created ? 'applied' : 'no-op', reasonCode: null }, { name: 'task:bind-pr', status: 'applied', reasonCode: null }],
|
|
1052
|
-
result: created ? 'pr_created' : 'pr_reused', error: null
|
|
1053
|
-
}), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'reused', createdByCurrentOperation: false });
|
|
1054
|
-
}
|
|
1055
|
-
function createPlatformPullRequest(taskRef, options) {
|
|
1056
|
-
const base = resolvedContext(taskRef, options);
|
|
1057
|
-
if (!base.ok)
|
|
1058
|
-
return withCreation(base.output, PRECONDITION_NOT_CREATED);
|
|
1059
|
-
if (!options.base || !options.head || !options.title.trim() || !options.body.trim()) {
|
|
1060
|
-
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1061
|
-
error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
|
|
1062
|
-
}), PRECONDITION_NOT_CREATED);
|
|
1063
|
-
}
|
|
1064
|
-
const phase = { value: 'before-post' };
|
|
1065
|
-
try {
|
|
1066
|
-
return withTaskExecutionLock(base.resolved.repoRoot, base.resolved.taskId, 'platform-pr.create', () => createPlatformPullRequestUnlocked(taskRef, { ...options, phase }));
|
|
1067
|
-
}
|
|
1068
|
-
catch (error) {
|
|
1069
|
-
if (error instanceof TaskExecutionLockError) {
|
|
1070
|
-
return withCreation(result('blocked', base.resolved.taskId, base.issueNumber, null, {
|
|
1071
|
-
platform: base.context.platform,
|
|
1072
|
-
capabilities: base.context.capabilities,
|
|
1073
|
-
error: { code: error.code, message: error.message, retryable: true }
|
|
1074
|
-
}), PRECONDITION_NOT_CREATED);
|
|
1075
|
-
}
|
|
1076
|
-
const creation = phase.value === 'post-accepted'
|
|
1077
|
-
? { kind: 'created', createdByCurrentOperation: true }
|
|
1078
|
-
: phase.value === 'post-dispatched'
|
|
1079
|
-
? { kind: 'unknown', errorCode: 'PR_CREATE_OUTCOME_UNKNOWN' }
|
|
1080
|
-
: PRECONDITION_NOT_CREATED;
|
|
1081
|
-
return withCreation(result(phase.value === 'post-dispatched' ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1221
|
+
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1082
1222
|
platform: base.context.platform,
|
|
1083
1223
|
capabilities: base.context.capabilities,
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1224
|
+
resource: { kind: 'pull-request', number: pullRequest.number },
|
|
1225
|
+
pullRequest,
|
|
1226
|
+
error: { code: 'PR_CREATED_BIND_FAILED', message: `${pullRequest.url}: ${bound.error.message}`, retryable: false }
|
|
1227
|
+
}), { kind: 'created', createdByCurrentOperation: true });
|
|
1228
|
+
return withCreation(result('applied', base.resolved.taskId, base.issueNumber, pullRequest.number, {
|
|
1229
|
+
changed: true,
|
|
1230
|
+
platform: base.context.platform,
|
|
1231
|
+
capabilities: base.context.capabilities,
|
|
1232
|
+
resource: { kind: 'pull-request', number: pullRequest.number },
|
|
1233
|
+
pullRequest,
|
|
1234
|
+
operations: [{ name: 'pr:create', status: 'applied', reasonCode: null }, { name: 'task:bind-pr', status: 'applied', reasonCode: null }],
|
|
1235
|
+
result: 'pr_created',
|
|
1236
|
+
error: null
|
|
1237
|
+
}), { kind: 'created', createdByCurrentOperation: true });
|
|
1091
1238
|
}
|
|
1092
|
-
function syncPlatformPullRequest(taskRef, options) {
|
|
1239
|
+
async function syncPlatformPullRequest(taskRef, options) {
|
|
1093
1240
|
const warningResult = warningResultForPrimary(options.primaryResult);
|
|
1094
1241
|
const softenFailure = (output) => {
|
|
1242
|
+
const authorityError = output.error?.code.startsWith('IN_LABEL_SYNC') || output.error?.code.startsWith('PR_IDENTITY');
|
|
1095
1243
|
const warning = output.error && output.task.prNumber !== null
|
|
1244
|
+
&& !authorityError
|
|
1096
1245
|
&& !['PR_NOT_LINKED', 'PR_BIND_CONFLICT'].includes(output.error.code)
|
|
1097
1246
|
? {
|
|
1098
1247
|
code: output.error.code,
|
|
@@ -1107,80 +1256,169 @@ function syncPlatformPullRequest(taskRef, options) {
|
|
|
1107
1256
|
? { ...output, status: 'applied', changed: false, error: null, result: warningResult, warnings: mergeOperationWarnings([warning]) }
|
|
1108
1257
|
: output;
|
|
1109
1258
|
};
|
|
1110
|
-
const base = resolvedContext(taskRef, options);
|
|
1259
|
+
const base = await resolvedContext(taskRef, options);
|
|
1111
1260
|
if (!base.ok)
|
|
1112
1261
|
return softenFailure(base.output);
|
|
1113
1262
|
if (!base.fact)
|
|
1114
1263
|
return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1115
1264
|
error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
|
|
1116
1265
|
}));
|
|
1117
|
-
if (!base.
|
|
1266
|
+
if (!base.prIdentity)
|
|
1118
1267
|
return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
1119
1268
|
error: { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false }
|
|
1120
1269
|
}));
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1270
|
+
{
|
|
1271
|
+
const inspected = await inspectExternalPullRequest(base, base.prIdentity);
|
|
1272
|
+
if (inspected.status === 'failed' || inspected.status === 'blocked' || !inspected.pullRequest)
|
|
1273
|
+
return softenFailure(inspected);
|
|
1274
|
+
if (!options.metadata && !options.closingIssue)
|
|
1275
|
+
return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1276
|
+
error: { code: 'PR_PAYLOAD_INVALID', message: 'sync requires a desired-state option', retryable: false }
|
|
1277
|
+
}));
|
|
1278
|
+
const issue = await inspectPlatformIssue(taskRef, { cwd: base.resolved.repoRoot, client: base.client });
|
|
1279
|
+
if (!issue.issue)
|
|
1280
|
+
return softenFailure(result(issue.status, base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1281
|
+
platform: base.context.platform, capabilities: base.context.capabilities, pullRequest: inspected.pullRequest, error: issue.error
|
|
1282
|
+
}));
|
|
1283
|
+
let inLabels = inspected.pullRequest.labels.filter((label) => label.startsWith('in:')).sort();
|
|
1284
|
+
let issueInLabelOperation = null;
|
|
1285
|
+
if (options.metadata && base.provider.issues?.listLabels) {
|
|
1286
|
+
const labels = await base.provider.issues.listLabels({ context: providerOperationContext(base.loadedContext) });
|
|
1287
|
+
if (!labels.ok)
|
|
1288
|
+
return softenFailure(providerPullRequestError(base, labels.error, base.prNumber));
|
|
1289
|
+
const target = taskInLabelTarget(base.resolved.repoRoot, base.frontmatter, inspected.pullRequest.labels, new Set(labels.value));
|
|
1290
|
+
if (!target.ok)
|
|
1291
|
+
return softenFailure(providerPullRequestError(base, target.error, base.prNumber));
|
|
1292
|
+
inLabels = target.value;
|
|
1293
|
+
const currentIssueLabels = issue.issue.labels.filter((label) => label.startsWith('in:')).sort();
|
|
1294
|
+
issueInLabelOperation = base.context.capabilities.triage
|
|
1295
|
+
? currentIssueLabels.join('\0') === inLabels.join('\0')
|
|
1296
|
+
? { name: 'labels:in:issue', status: 'no-op', reasonCode: null }
|
|
1297
|
+
: { name: 'labels:in:issue', status: 'planned', reasonCode: null }
|
|
1298
|
+
: { name: 'labels:in:issue', status: 'skipped', reasonCode: 'TRIAGE_REQUIRED' };
|
|
1299
|
+
}
|
|
1300
|
+
const planned = planPullRequestMetadata({
|
|
1301
|
+
pullRequest: inspected.pullRequest,
|
|
1302
|
+
issue: issue.issue,
|
|
1303
|
+
taskType: String(base.frontmatter.type || 'task'),
|
|
1304
|
+
issueNumber: base.issueNumber,
|
|
1305
|
+
capabilities: base.context.capabilities,
|
|
1306
|
+
inLabels
|
|
1307
|
+
}).operations.filter((operation) => options.metadata || operation.name === 'closing-issue')
|
|
1308
|
+
.filter((operation) => options.closingIssue || operation.name !== 'closing-issue');
|
|
1309
|
+
const operations = [
|
|
1310
|
+
...(issueInLabelOperation ? [issueInLabelOperation] : []),
|
|
1311
|
+
...planned.map(({ name, status, reasonCode }) => ({ name, status, reasonCode }))
|
|
1312
|
+
];
|
|
1313
|
+
const milestoneOperation = planned.find((operation) => operation.name === 'milestone' && operation.status === 'planned');
|
|
1314
|
+
if (milestoneOperation && base.provider.issues?.listMilestones) {
|
|
1315
|
+
const milestones = await base.provider.issues.listMilestones({ context: providerOperationContext(base.loadedContext) });
|
|
1162
1316
|
if (!milestones.ok)
|
|
1163
|
-
return
|
|
1164
|
-
const flat = milestones.value.flatMap((entry) => Array.isArray(entry) ? entry : [entry]);
|
|
1165
|
-
payload.milestone = flat.find((entry) => entry.title === operation.value)?.number ?? null;
|
|
1317
|
+
return providerPullRequestError(base, milestones.error, base.prNumber);
|
|
1166
1318
|
}
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1319
|
+
if (options.dryRun)
|
|
1320
|
+
return softenFailure(result(planned.some((operation) => operation.status === 'planned') ? 'planned' : 'no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1321
|
+
platform: base.context.platform, capabilities: base.context.capabilities, pullRequest: inspected.pullRequest, operations, error: null
|
|
1322
|
+
}));
|
|
1323
|
+
let issueChanged = false;
|
|
1324
|
+
if (issueInLabelOperation?.status === 'planned') {
|
|
1325
|
+
const desiredIssueLabels = [...new Set([
|
|
1326
|
+
...issue.issue.labels.filter((label) => !label.startsWith('in:')),
|
|
1327
|
+
...inLabels
|
|
1328
|
+
])].sort();
|
|
1329
|
+
const updatedIssue = base.provider.issues?.update
|
|
1330
|
+
? await base.provider.issues.update({
|
|
1331
|
+
context: providerOperationContext(base.loadedContext),
|
|
1332
|
+
target: base.issueIdentity,
|
|
1333
|
+
currentLabels: issue.issue.labels,
|
|
1334
|
+
patch: { labels: desiredIssueLabels },
|
|
1335
|
+
mutation: { idempotencyKey: `issue:update:${base.resolved.taskId}:in-labels` }
|
|
1336
|
+
})
|
|
1337
|
+
: unsupportedProviderOperation(base.provider, 'issues.update');
|
|
1338
|
+
if (!updatedIssue.ok)
|
|
1339
|
+
return softenFailure(providerPullRequestError(base, updatedIssue.error, base.prNumber));
|
|
1340
|
+
issueChanged = updatedIssue.value.changed;
|
|
1341
|
+
const rereadIssue = base.provider.issues?.inspect
|
|
1342
|
+
? await base.provider.issues.inspect({ context: providerOperationContext(base.loadedContext), target: base.issueIdentity })
|
|
1343
|
+
: unsupportedProviderOperation(base.provider, 'issues.inspect');
|
|
1344
|
+
if (!rereadIssue.ok)
|
|
1345
|
+
return softenFailure(providerPullRequestError(base, {
|
|
1346
|
+
...rereadIssue.error,
|
|
1347
|
+
code: 'IN_LABEL_SYNC_PARTIAL',
|
|
1348
|
+
message: `In-label synchronization is partial or unknown: ${rereadIssue.error.message}`
|
|
1349
|
+
}, base.prNumber));
|
|
1350
|
+
const actualIssueLabels = rereadIssue.value.labels.filter((label) => label.startsWith('in:')).sort();
|
|
1351
|
+
if (actualIssueLabels.join('\0') !== inLabels.join('\0'))
|
|
1352
|
+
return softenFailure(providerPullRequestError(base, {
|
|
1353
|
+
code: 'IN_LABEL_SYNC_PARTIAL',
|
|
1354
|
+
message: 'Issue in: labels did not converge after update',
|
|
1355
|
+
retryable: true
|
|
1356
|
+
}, base.prNumber));
|
|
1357
|
+
}
|
|
1358
|
+
const patch = {};
|
|
1359
|
+
for (const operation of planned) {
|
|
1360
|
+
if (operation.status !== 'planned')
|
|
1361
|
+
continue;
|
|
1362
|
+
if (operation.name === 'labels')
|
|
1363
|
+
patch.labels = operation.value;
|
|
1364
|
+
if (operation.name === 'assignees')
|
|
1365
|
+
patch.assignees = operation.value;
|
|
1366
|
+
if (operation.name === 'closing-issue')
|
|
1367
|
+
patch.body = operation.value;
|
|
1368
|
+
if (operation.name === 'milestone')
|
|
1369
|
+
patch.milestone = operation.value;
|
|
1370
|
+
}
|
|
1371
|
+
if (Object.keys(patch).length === 0)
|
|
1372
|
+
return softenFailure(result(planned.some((operation) => operation.status === 'skipped') ? 'degraded' : 'no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1373
|
+
platform: base.context.platform, capabilities: base.context.capabilities, resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: inspected.pullRequest, operations, error: null
|
|
1374
|
+
}));
|
|
1375
|
+
const updated = base.provider.changeRequests?.update
|
|
1376
|
+
? await base.provider.changeRequests.update({
|
|
1377
|
+
context: providerOperationContext(base.loadedContext),
|
|
1378
|
+
target: base.prIdentity,
|
|
1379
|
+
currentLabels: inspected.pullRequest.labels,
|
|
1380
|
+
patch: patch,
|
|
1381
|
+
mutation: { idempotencyKey: `pull-request:update:${base.resolved.taskId}` }
|
|
1382
|
+
})
|
|
1383
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.update');
|
|
1384
|
+
if (!updated.ok) {
|
|
1385
|
+
const error = issueChanged && !updated.error.code.startsWith('IN_LABEL_SYNC')
|
|
1386
|
+
? { ...updated.error, code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${updated.error.message}` }
|
|
1387
|
+
: updated.error;
|
|
1388
|
+
return softenFailure(providerPullRequestError(base, error, base.prNumber));
|
|
1389
|
+
}
|
|
1390
|
+
let finalPullRequest = inspected.pullRequest;
|
|
1391
|
+
if (planned.some((operation) => operation.name === 'labels' && operation.status === 'planned')) {
|
|
1392
|
+
const reread = base.provider.changeRequests?.inspect
|
|
1393
|
+
? await base.provider.changeRequests.inspect({ context: providerOperationContext(base.loadedContext), target: base.prIdentity })
|
|
1394
|
+
: unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
|
|
1395
|
+
if (!reread.ok)
|
|
1396
|
+
return softenFailure(providerPullRequestError(base, {
|
|
1397
|
+
...reread.error,
|
|
1398
|
+
code: 'IN_LABEL_SYNC_PARTIAL',
|
|
1399
|
+
message: `In-label synchronization is partial or unknown: ${reread.error.message}`
|
|
1400
|
+
}, base.prNumber));
|
|
1401
|
+
finalPullRequest = normalizeProviderPullRequest(reread.value, base.context.platform.repository, base.prNumber || 0, inspected.pullRequest);
|
|
1402
|
+
const expected = ((planned.find((operation) => operation.name === 'labels')?.value || [])
|
|
1403
|
+
.filter((label) => label.startsWith('in:') || label.startsWith('type:'))).sort();
|
|
1404
|
+
const actual = finalPullRequest.labels.filter((label) => label.startsWith('in:') || label.startsWith('type:')).sort();
|
|
1405
|
+
if (actual.join('\0') !== expected.join('\0'))
|
|
1406
|
+
return softenFailure(providerPullRequestError(base, {
|
|
1407
|
+
code: 'IN_LABEL_SYNC_PARTIAL',
|
|
1408
|
+
message: 'Pull request labels did not converge after update',
|
|
1409
|
+
retryable: true
|
|
1410
|
+
}, base.prNumber));
|
|
1411
|
+
}
|
|
1412
|
+
return softenFailure(result(planned.some((operation) => operation.status === 'skipped') ? 'degraded' : 'applied', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1413
|
+
changed: issueChanged || updated.value.changed,
|
|
1414
|
+
platform: base.context.platform,
|
|
1415
|
+
capabilities: base.context.capabilities,
|
|
1416
|
+
resource: { kind: 'pull-request', number: base.prNumber },
|
|
1417
|
+
pullRequest: finalPullRequest,
|
|
1418
|
+
operations: operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation),
|
|
1419
|
+
error: null
|
|
1172
1420
|
}));
|
|
1173
1421
|
}
|
|
1174
|
-
const patched = base.client.json(['api', `repos/${base.context.platform.repository}/issues/${base.prNumber}`, '-X', 'PATCH', '--input', '-'], {
|
|
1175
|
-
cwd: base.resolved.repoRoot, method: 'PATCH', input: JSON.stringify(payload)
|
|
1176
|
-
});
|
|
1177
|
-
if (!patched.ok)
|
|
1178
|
-
return softenFailure(result(patched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { pullRequest: fetched.value, operations, error: patched.error }));
|
|
1179
|
-
return softenFailure(result(planned.some((operation) => operation.status === 'skipped') ? 'degraded' : 'applied', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
1180
|
-
changed: true, platform: base.context.platform, capabilities: base.context.capabilities,
|
|
1181
|
-
resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: fetched.value,
|
|
1182
|
-
operations: operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation), error: null
|
|
1183
|
-
}));
|
|
1184
1422
|
}
|
|
1185
1423
|
function readProjectPrFlow(repoRoot) {
|
|
1186
1424
|
try {
|
|
@@ -1203,12 +1441,18 @@ function skipPlatformPullRequestFactUnlocked(resolved, options) {
|
|
|
1203
1441
|
error: { code: 'TASK_DOCUMENT_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
1204
1442
|
});
|
|
1205
1443
|
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1444
|
+
let issueIdentity;
|
|
1445
|
+
try {
|
|
1446
|
+
issueIdentity = taskIssueIdentity(frontmatter, undefined, options.runtimeVersion);
|
|
1447
|
+
}
|
|
1448
|
+
catch (error) {
|
|
1449
|
+
return result('failed', resolved.taskId, null, null, { error: { ...taskIssueIdentityError(error), retryable: false } });
|
|
1450
|
+
}
|
|
1451
|
+
const issueNumber = resourceIdentityNumber(issueIdentity);
|
|
1452
|
+
const existing = readPrDeliveryFact(frontmatter, options.runtimeVersion);
|
|
1209
1453
|
if (existing.status === 'invalid')
|
|
1210
1454
|
return result('failed', resolved.taskId, issueNumber, null, {
|
|
1211
|
-
error: { code:
|
|
1455
|
+
error: { code: existing.error.code, message: existing.error.message, retryable: false }
|
|
1212
1456
|
});
|
|
1213
1457
|
if (existing.status === 'missing')
|
|
1214
1458
|
return result('failed', resolved.taskId, issueNumber, null, {
|
|
@@ -1219,10 +1463,12 @@ function skipPlatformPullRequestFactUnlocked(resolved, options) {
|
|
|
1219
1463
|
operations: [{ name: 'task:skip-pr', status: 'no-op', reasonCode: null }],
|
|
1220
1464
|
error: null
|
|
1221
1465
|
});
|
|
1222
|
-
if (existing.fact.state === 'bound')
|
|
1223
|
-
|
|
1224
|
-
|
|
1466
|
+
if (existing.fact.state === 'bound') {
|
|
1467
|
+
const boundNumber = resourceIdentityNumber(existing.fact.identity.resource);
|
|
1468
|
+
return result('failed', resolved.taskId, issueNumber, boundNumber, {
|
|
1469
|
+
error: { code: 'PR_DELIVERY_FACT_CONFLICT', message: `Task is already bound to PR ${boundNumber === null ? 'identity' : `#${boundNumber}`}`, retryable: false }
|
|
1225
1470
|
});
|
|
1471
|
+
}
|
|
1226
1472
|
if (readProjectPrFlow(resolved.repoRoot) === 'required')
|
|
1227
1473
|
return result('failed', resolved.taskId, issueNumber, null, {
|
|
1228
1474
|
error: { code: 'PR_SKIP_FORBIDDEN', message: 'Cannot skip PR delivery when prFlow is required', retryable: false }
|
|
@@ -1249,7 +1495,7 @@ function skipPlatformPullRequestFactUnlocked(resolved, options) {
|
|
|
1249
1495
|
error: null
|
|
1250
1496
|
});
|
|
1251
1497
|
}
|
|
1252
|
-
function skipPlatformPullRequestFact(taskRef, options) {
|
|
1498
|
+
async function skipPlatformPullRequestFact(taskRef, options) {
|
|
1253
1499
|
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
1254
1500
|
if (!resolved.ok)
|
|
1255
1501
|
return result('failed', resolved.taskId, null, null, {
|
|
@@ -1272,6 +1518,6 @@ function skipPlatformPullRequestFact(taskRef, options) {
|
|
|
1272
1518
|
});
|
|
1273
1519
|
}
|
|
1274
1520
|
}
|
|
1275
|
-
export { bindPlatformPullRequest, createPlatformPullRequest, inspectGitHubPullRequest, inspectGitHubIssueClosingChangeRequests, inspectPlatformPullRequest, inspectPlatformPullRequestByNumber, normalizePullRequest,
|
|
1521
|
+
export { bindPlatformPullRequest, createPlatformPullRequest, inspectGitHubPullRequest, inspectGitHubIssueClosingChangeRequests, inspectPlatformPullRequest, inspectPlatformPullRequestByNumber, normalizePullRequest, resolveExternalPullRequest, skipPlatformPullRequestFact, selectExternalPullRequest, selectPullRequest, syncPlatformPullRequest, syncPlatformPullRequestInLabels };
|
|
1276
1522
|
export { warningResultForPrimary };
|
|
1277
1523
|
//# sourceMappingURL=pull-requests.js.map
|