@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
|
@@ -3,21 +3,22 @@ import path from 'node:path';
|
|
|
3
3
|
import { execFileSync } from 'node:child_process';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from './
|
|
11
|
-
import type { ReleaseNoteActor } from './github-release-notes.ts';
|
|
12
|
-
import type { GitHubClient } from './github-client.ts';
|
|
6
|
+
import { resolvePlatformProviderContext } from './context.ts';
|
|
7
|
+
import type { PlatformClient } from './context.ts';
|
|
8
|
+
import { providerError, providerOperationContext, providerStatus, unsupportedProviderOperation } from './provider-bridge.ts';
|
|
9
|
+
import { resourceIdentityNumber } from './resource-identity.ts';
|
|
10
|
+
import type { ResourceIdentity } from './resource-identity.ts';
|
|
13
11
|
|
|
14
12
|
type ReleaseNoteOptions = {
|
|
15
13
|
cwd?: string;
|
|
16
14
|
platformType?: string;
|
|
17
|
-
client?:
|
|
15
|
+
client?: PlatformClient;
|
|
18
16
|
};
|
|
19
17
|
|
|
18
|
+
type ReleaseNoteActor = { id?: string; name?: string };
|
|
20
19
|
type ReleaseNoteCommit = { oid: string; title: string; authors: ReleaseNoteActor[] };
|
|
20
|
+
type ReleaseNoteIssue = { number: number; identity?: ResourceIdentity; title: string; url: string; labels: string[]; author: { login: string; bot: boolean } | null };
|
|
21
|
+
type ReleaseNotePullRequest = { number: number; identity?: ResourceIdentity; title: string; body: string; url: string; mergedAt: string; labels: string[]; author: { login: string; bot: boolean } | null; closingIssues: ReleaseNoteIssue[] };
|
|
21
22
|
|
|
22
23
|
const unsupportedError = {
|
|
23
24
|
code: 'PLATFORM_RELEASE_NOTES_UNSUPPORTED',
|
|
@@ -130,7 +131,7 @@ function configuredPlatform(cwd: string): string | null {
|
|
|
130
131
|
}
|
|
131
132
|
}
|
|
132
133
|
|
|
133
|
-
function releaseNoteContext(
|
|
134
|
+
async function releaseNoteContext(
|
|
134
135
|
input: { fromTag: string; toTag: string; branch: string; historyLimit?: number },
|
|
135
136
|
options: ReleaseNoteOptions = {}
|
|
136
137
|
) {
|
|
@@ -140,7 +141,13 @@ function releaseNoteContext(
|
|
|
140
141
|
}
|
|
141
142
|
const cwd = path.resolve(options.cwd || process.cwd());
|
|
142
143
|
const platformType = options.platformType ?? configuredPlatform(cwd);
|
|
143
|
-
|
|
144
|
+
const loaded = await resolvePlatformProviderContext({ cwd, platformType: platformType || undefined, client: options.client });
|
|
145
|
+
if (!loaded.ok) {
|
|
146
|
+
if (loaded.context.status === 'no-op') return baseResult('no-op');
|
|
147
|
+
const status = loaded.context.status === 'blocked' ? 'blocked' : 'failed';
|
|
148
|
+
return { ...baseResult(status), error: loaded.context.error };
|
|
149
|
+
}
|
|
150
|
+
if (!loaded.value.context.platform.repository) return baseResult('no-op');
|
|
144
151
|
let fromOid: string;
|
|
145
152
|
let toOid: string;
|
|
146
153
|
let fromTime: string;
|
|
@@ -160,75 +167,61 @@ function releaseNoteContext(
|
|
|
160
167
|
} catch {
|
|
161
168
|
return { ...baseResult('failed'), error: { code: 'RELEASE_NOTES_RANGE_INVALID', message: 'The release tag range is missing or is not ancestral', retryable: false } };
|
|
162
169
|
}
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
...baseResult(context.status === 'blocked' ? 'blocked' : 'failed'),
|
|
167
|
-
error: context.error || { code: 'PLATFORM_CONTEXT_UNAVAILABLE', message: 'Platform repository is unavailable', retryable: context.status === 'blocked' }
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
const platform = fetchGitHubReleaseNoteData(
|
|
171
|
-
{
|
|
172
|
-
repository: context.platform.repository,
|
|
170
|
+
const platform = loaded.value.provider.releases?.collectNotes
|
|
171
|
+
? await loaded.value.provider.releases.collectNotes({
|
|
172
|
+
context: providerOperationContext(loaded.value),
|
|
173
173
|
commitOids: commitFacts.map((item) => item.oid),
|
|
174
174
|
branch: input.branch,
|
|
175
175
|
historyLimit,
|
|
176
176
|
fromTime,
|
|
177
177
|
toTime
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
);
|
|
181
|
-
if (platform.status === 'blocked' || platform.status === 'failed' || !('authors' in platform)) {
|
|
182
|
-
return { ...baseResult(platform.status), error: platform.error };
|
|
183
|
-
}
|
|
178
|
+
})
|
|
179
|
+
: unsupportedProviderOperation(loaded.value.provider, 'releases.collectNotes');
|
|
180
|
+
if (!platform.ok) return { ...baseResult(platform.error.retryable ? 'blocked' : 'failed'), error: providerError(platform.error, 'PLATFORM_PROVIDER_OPERATION_FAILED') };
|
|
184
181
|
const commits = commitFacts.map((commit) => {
|
|
185
182
|
const seen = new Set<string>();
|
|
186
|
-
const authors =
|
|
187
|
-
|
|
183
|
+
const authors = platform.value.history.filter((item) => item.sha === commit.oid).flatMap((item) => item.author ? [item.author] : [])
|
|
184
|
+
.concat(platform.value.actors)
|
|
185
|
+
.filter((actor) => {
|
|
186
|
+
const key = actor.id || actor.name || '';
|
|
188
187
|
if (seen.has(key)) return false;
|
|
189
188
|
seen.add(key);
|
|
190
189
|
return true;
|
|
191
|
-
|
|
190
|
+
});
|
|
192
191
|
return { ...commit, authors };
|
|
193
192
|
});
|
|
194
|
-
const pullRequests = platform.
|
|
193
|
+
const pullRequests: ReleaseNotePullRequest[] = platform.value.mergedPullRequests
|
|
195
194
|
.map((item) => ({
|
|
196
|
-
number:
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
?
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
labels: Array.isArray(value.labels) ? value.labels.map((label) => String((label as { name?: string }).name || label)) : [],
|
|
213
|
-
author: value.author && typeof value.author === 'object'
|
|
214
|
-
? { login: String((value.author as { login?: string }).login || '').toLowerCase(), bot: String((value.author as { login?: string }).login || '').endsWith('[bot]') }
|
|
215
|
-
: null
|
|
216
|
-
};
|
|
217
|
-
}) : []
|
|
195
|
+
number: item.number || resourceIdentityNumber(item.identity) || 0,
|
|
196
|
+
...(item.identity ? { identity: item.identity } : {}),
|
|
197
|
+
title: item.title,
|
|
198
|
+
body: item.body,
|
|
199
|
+
url: item.displayUrl || '',
|
|
200
|
+
mergedAt: item.mergedAt || '',
|
|
201
|
+
labels: item.labels || [],
|
|
202
|
+
author: item.author ? { login: item.author.name || item.author.id || '', bot: false } : null,
|
|
203
|
+
closingIssues: platform.value.closingIssues.filter((issue) => issue.id === item.id).map((issue) => ({
|
|
204
|
+
number: issue.number || resourceIdentityNumber(issue.identity) || 0,
|
|
205
|
+
...(issue.identity ? { identity: issue.identity } : {}),
|
|
206
|
+
title: issue.title,
|
|
207
|
+
url: issue.displayUrl || '',
|
|
208
|
+
labels: issue.labels || [],
|
|
209
|
+
author: issue.author ? { login: issue.author.name || issue.author.id || '', bot: false } : null
|
|
210
|
+
}))
|
|
218
211
|
}))
|
|
219
212
|
.sort((left, right) => left.number - right.number);
|
|
220
213
|
return {
|
|
221
214
|
status: 'no-op' as const,
|
|
222
215
|
changed: false,
|
|
223
216
|
range: { fromTag: input.fromTag, toTag: input.toTag, fromOid, toOid },
|
|
224
|
-
history: platform.history,
|
|
217
|
+
history: platform.value.history,
|
|
225
218
|
pullRequests,
|
|
226
219
|
commits,
|
|
227
220
|
error: null
|
|
228
221
|
};
|
|
229
222
|
}
|
|
230
223
|
|
|
231
|
-
function publishReleaseNotes(
|
|
224
|
+
async function publishReleaseNotes(
|
|
232
225
|
input: { tag: string; title: string; notesFile: string; expectedSha256: string; dryRun?: boolean },
|
|
233
226
|
options: ReleaseNoteOptions = {}
|
|
234
227
|
) {
|
|
@@ -246,12 +239,36 @@ function publishReleaseNotes(
|
|
|
246
239
|
}
|
|
247
240
|
const cwd = path.resolve(options.cwd || process.cwd());
|
|
248
241
|
const platformType = options.platformType ?? configuredPlatform(cwd);
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
242
|
+
const loaded = await resolvePlatformProviderContext({ cwd, platformType: platformType || undefined, client: options.client });
|
|
243
|
+
if (!loaded.ok) {
|
|
244
|
+
const status = loaded.context.status === 'blocked'
|
|
245
|
+
? 'blocked'
|
|
246
|
+
: loaded.context.status === 'no-op' ? 'no-op' : 'failed';
|
|
247
|
+
return {
|
|
248
|
+
...baseResult(status),
|
|
249
|
+
error: loaded.context.error || { code: 'PLATFORM_PROVIDER_LOAD_FAILED', message: 'Selected provider failed to load', retryable: false }
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
if (loaded.ok) {
|
|
253
|
+
const context = loaded.value.context;
|
|
254
|
+
if (!context.platform.repository) return { status: context.status === 'blocked' ? 'blocked' as const : 'failed' as const, changed: false, operation: null, url: null, error: context.error };
|
|
255
|
+
const published = loaded.value.provider.releases?.publishNotes
|
|
256
|
+
? await loaded.value.provider.releases.publishNotes({
|
|
257
|
+
context: providerOperationContext(loaded.value),
|
|
258
|
+
release: { kind: 'key', value: input.tag },
|
|
259
|
+
title: input.title,
|
|
260
|
+
notes: {
|
|
261
|
+
text: fs.readFileSync(input.notesFile, 'utf8'),
|
|
262
|
+
sha256: actualSha256,
|
|
263
|
+
byteLength: fs.statSync(input.notesFile).size
|
|
264
|
+
},
|
|
265
|
+
mutation: { idempotencyKey: `release-notes:publish:${input.tag}` }
|
|
266
|
+
})
|
|
267
|
+
: unsupportedProviderOperation(loaded.value.provider, 'releases.publishNotes');
|
|
268
|
+
if (!published.ok) return { status: providerStatus(published.error), changed: false, operation: null, url: null, error: providerError(published.error, 'PLATFORM_PROVIDER_OPERATION_FAILED') };
|
|
269
|
+
return { status: 'applied' as const, changed: published.value.changed, operation: 'publish-notes', url: null, error: null };
|
|
253
270
|
}
|
|
254
|
-
return
|
|
271
|
+
return { status: 'failed' as const, changed: false, operation: null, url: null, error: { code: 'PLATFORM_PROVIDER_LOAD_FAILED', message: 'Selected provider failed to load', retryable: false } };
|
|
255
272
|
}
|
|
256
273
|
|
|
257
274
|
export {
|
package/lib/platform/releases.ts
CHANGED
|
@@ -1,52 +1,78 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import { resolvePlatformProviderContext } from './context.ts';
|
|
4
|
+
import type { PlatformClient } from './context.ts';
|
|
4
5
|
import { platformResult } from './types.ts';
|
|
6
|
+
import { providerError, providerOperationContext, providerStatus, unsupportedProviderOperation } from './provider-bridge.ts';
|
|
5
7
|
|
|
6
8
|
type ReleaseSnapshot = { tag: string; published: boolean; draft: boolean; url: string | null };
|
|
7
|
-
type ReleaseOptions = { cwd?: string; client?:
|
|
9
|
+
type ReleaseOptions = { cwd?: string; client?: PlatformClient };
|
|
8
10
|
|
|
9
11
|
function flattenPages(value: unknown): Array<Record<string, unknown>> {
|
|
10
12
|
if (!Array.isArray(value)) return [];
|
|
11
13
|
return value.flatMap((entry) => Array.isArray(entry) ? entry : [entry]).filter((entry): entry is Record<string, unknown> => Boolean(entry) && typeof entry === 'object');
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
function inspectPlatformRelease(tag: string, options: ReleaseOptions = {}) {
|
|
15
|
-
const
|
|
16
|
-
const context =
|
|
16
|
+
async function inspectPlatformRelease(tag: string, options: ReleaseOptions = {}) {
|
|
17
|
+
const loaded = await resolvePlatformProviderContext({ cwd: options.cwd, client: options.client });
|
|
18
|
+
const context = loaded.ok ? loaded.value.context : loaded.context;
|
|
17
19
|
if (!context.platform.repository) return { ...platformResult(context.status), platform: context.platform, release: null, workflows: [], error: context.error };
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
if (loaded.ok) {
|
|
21
|
+
const inspected = loaded.value.provider.releases?.inspect
|
|
22
|
+
? await loaded.value.provider.releases.inspect({ context: providerOperationContext(loaded.value), tag })
|
|
23
|
+
: unsupportedProviderOperation(loaded.value.provider, 'releases.inspect');
|
|
24
|
+
if (!inspected.ok) {
|
|
25
|
+
if (inspected.error.code === 'RESOURCE_NOT_FOUND') return { ...platformResult('no-op'), platform: context.platform, release: null, workflows: [], error: null };
|
|
26
|
+
return { ...platformResult(providerStatus(inspected.error)), platform: context.platform, release: null, workflows: [], error: providerError(inspected.error, 'PLATFORM_PROVIDER_OPERATION_FAILED') };
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
...platformResult('no-op'),
|
|
30
|
+
platform: context.platform,
|
|
31
|
+
release: { tag: inspected.value.tag, published: !inspected.value.draft, draft: inspected.value.draft, url: inspected.value.displayUrl || null },
|
|
32
|
+
workflows: inspected.value.workflows || [],
|
|
33
|
+
error: null
|
|
34
|
+
};
|
|
22
35
|
}
|
|
23
|
-
|
|
24
|
-
const runs = client.json<unknown[]>(['run', 'list', '--repo', context.platform.repository, '--limit', '100', '--json', 'name,workflowName,displayTitle,event,headBranch,headSha,status,conclusion,createdAt,databaseId,attempt,url'], { cwd: options.cwd });
|
|
25
|
-
return { ...platformResult(runs.ok ? 'no-op' : runs.error.retryable ? 'blocked' : 'degraded'), platform: context.platform, release, workflows: runs.ok ? runs.value : [], error: runs.ok ? null : runs.error };
|
|
36
|
+
return { ...platformResult('failed'), platform: context.platform, release: null, workflows: [], error: context.error };
|
|
26
37
|
}
|
|
27
38
|
|
|
28
|
-
function upsertPlatformRelease(input: { tag: string; title?: string; notesFile?: string }, options: ReleaseOptions = {}) {
|
|
29
|
-
const inspected = inspectPlatformRelease(input.tag, options);
|
|
39
|
+
async function upsertPlatformRelease(input: { tag: string; title?: string; notesFile?: string }, options: ReleaseOptions = {}) {
|
|
40
|
+
const inspected = await inspectPlatformRelease(input.tag, options);
|
|
30
41
|
if (inspected.release?.published) return { ...inspected, status: 'no-op' as const, changed: false, operations: [] };
|
|
31
42
|
if (inspected.status === 'blocked' || inspected.status === 'failed') return { ...inspected, changed: false, operations: [] };
|
|
32
43
|
if (!inspected.platform.repository) return { ...inspected, status: 'no-op' as const, changed: false, operations: [] };
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
const loaded = await resolvePlatformProviderContext({ cwd: options.cwd, client: options.client });
|
|
45
|
+
if (loaded.ok) {
|
|
46
|
+
let notes: { text: string; sha256: string; byteLength: number } | undefined;
|
|
47
|
+
if (input.notesFile) {
|
|
48
|
+
try {
|
|
49
|
+
const bytes = fs.readFileSync(input.notesFile);
|
|
50
|
+
notes = { text: bytes.toString('utf8'), sha256: `sha256:${crypto.createHash('sha256').update(bytes).digest('hex')}`, byteLength: bytes.byteLength };
|
|
51
|
+
} catch {
|
|
52
|
+
return { ...inspected, status: 'failed' as const, changed: false, operations: [{ name: 'create-release', status: 'failed' }], error: { code: 'RELEASE_NOTES_FILE_UNREADABLE', message: 'notes-file could not be read', retryable: false } };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const created = loaded.value.provider.releases?.create
|
|
56
|
+
? await loaded.value.provider.releases.create({
|
|
57
|
+
context: providerOperationContext(loaded.value),
|
|
58
|
+
tag: input.tag,
|
|
59
|
+
title: input.title ?? input.tag,
|
|
60
|
+
...(notes ? { notes } : {}),
|
|
61
|
+
mutation: { idempotencyKey: `release:create:${input.tag}` }
|
|
62
|
+
})
|
|
63
|
+
: unsupportedProviderOperation(loaded.value.provider, 'releases.create');
|
|
64
|
+
if (!created.ok) return { ...inspected, status: providerStatus(created.error), changed: false, operations: [{ name: 'create-release', status: 'failed' }], error: providerError(created.error, 'PLATFORM_PROVIDER_OPERATION_FAILED') };
|
|
65
|
+
return { ...inspected, status: 'applied' as const, changed: created.value.changed, operations: [{ name: 'create-release', status: 'applied' }], error: null };
|
|
66
|
+
}
|
|
67
|
+
return { ...inspected, status: 'failed' as const, changed: false, operations: [{ name: 'create-release', status: 'failed' }], error: { code: 'PLATFORM_PROVIDER_LOAD_FAILED', message: 'Selected provider failed to load', retryable: false } };
|
|
39
68
|
}
|
|
40
69
|
|
|
41
|
-
function reconcileReleaseMilestones(version: string, options: ReleaseOptions = {}) {
|
|
70
|
+
async function reconcileReleaseMilestones(version: string, options: ReleaseOptions = {}) {
|
|
42
71
|
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
|
|
43
72
|
if (!match) return { status: 'failed' as const, changed: false, operations: [], error: { code: 'RELEASE_VERSION_INVALID', message: `Invalid version '${version}'` } };
|
|
44
|
-
const
|
|
45
|
-
const context =
|
|
73
|
+
const loaded = await resolvePlatformProviderContext({ cwd: options.cwd, client: options.client });
|
|
74
|
+
const context = loaded.ok ? loaded.value.context : loaded.context;
|
|
46
75
|
if (!context.platform.repository) return { ...platformResult(context.status), changed: false, operations: [], error: context.error };
|
|
47
|
-
const listed = client.json<unknown>(['api', '--paginate', '--slurp', `repos/${context.platform.repository}/milestones?state=all&per_page=100`], { cwd: options.cwd });
|
|
48
|
-
if (!listed.ok) return { ...platformResult(listed.error.retryable ? 'blocked' : 'failed'), changed: false, operations: [], error: listed.error };
|
|
49
|
-
const milestones = flattenPages(listed.value).map((item) => ({ title: String(item.title || ''), number: Number(item.number), state: String(item.state || '') }));
|
|
50
76
|
const major = Number(match[1]); const minor = Number(match[2]); const patch = Number(match[3]);
|
|
51
77
|
const nextPatch = `${major}.${minor}.${patch + 1}`;
|
|
52
78
|
const currentLine = `${major}.${minor}.x`;
|
|
@@ -62,22 +88,30 @@ function reconcileReleaseMilestones(version: string, options: ReleaseOptions = {
|
|
|
62
88
|
{ title: nextLine, description: `Issues that we want to resolve in ${major}.${minor + 1} line.` }
|
|
63
89
|
);
|
|
64
90
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
91
|
+
if (loaded.ok) {
|
|
92
|
+
const reconciled = loaded.value.provider.releases?.reconcileMilestones
|
|
93
|
+
? await loaded.value.provider.releases.reconcileMilestones({
|
|
94
|
+
context: providerOperationContext(loaded.value),
|
|
95
|
+
version,
|
|
96
|
+
desired: [
|
|
97
|
+
{ key: version, title: version, description: `Issues that we want to resolve in v${version}.`, state: 'closed' },
|
|
98
|
+
...ensure.map((item) => ({ key: item.title, title: item.title, description: item.description, state: 'open' as const }))
|
|
99
|
+
],
|
|
100
|
+
mutation: { idempotencyKey: `milestones:reconcile:${version}` }
|
|
101
|
+
})
|
|
102
|
+
: unsupportedProviderOperation(loaded.value.provider, 'releases.reconcileMilestones');
|
|
103
|
+
if (!reconciled.ok) return { status: providerStatus(reconciled.error), changed: false, operations: [], error: providerError(reconciled.error, 'PLATFORM_PROVIDER_OPERATION_FAILED') };
|
|
104
|
+
return {
|
|
105
|
+
status: reconciled.value.changed ? 'applied' as const : 'no-op' as const,
|
|
106
|
+
changed: reconciled.value.changed,
|
|
107
|
+
operations: [
|
|
108
|
+
...reconciled.value.closed.map((title) => ({ name: 'close-milestone', title, status: 'applied' as const })),
|
|
109
|
+
...reconciled.value.created.map((title) => ({ name: 'ensure-milestone', title, status: 'applied' as const }))
|
|
110
|
+
],
|
|
111
|
+
error: null
|
|
112
|
+
};
|
|
77
113
|
}
|
|
78
|
-
|
|
79
|
-
const changed = operations.some((operation) => operation.status === 'applied');
|
|
80
|
-
return { status: failed ? 'failed' as const : changed ? 'applied' as const : 'no-op' as const, changed, operations, error: failed ? { code: 'MILESTONE_RECONCILE_FAILED', message: 'One or more milestone operations failed' } : null };
|
|
114
|
+
return { status: 'failed' as const, changed: false, operations: [], error: { code: 'PLATFORM_CAPABILITY_UNSUPPORTED', message: 'Release milestone reconciliation is unavailable', retryable: false } };
|
|
81
115
|
}
|
|
82
116
|
|
|
83
117
|
export { inspectPlatformRelease, reconcileReleaseMilestones, upsertPlatformRelease };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
type ResourceIdentityKind = 'id' | 'number' | 'key';
|
|
2
|
+
type ResourceIdentity =
|
|
3
|
+
| { kind: 'id'; value: string }
|
|
4
|
+
| { kind: 'number'; value: number }
|
|
5
|
+
| { kind: 'key'; value: string };
|
|
6
|
+
type CanonicalResourceIdentity = ResourceIdentity;
|
|
7
|
+
|
|
8
|
+
type PlatformResourceKind = 'issue' | 'pull-request' | 'comment' | 'release';
|
|
9
|
+
type ProviderIdentityDeclaration = Partial<Record<PlatformResourceKind, ResourceIdentityKind>>;
|
|
10
|
+
|
|
11
|
+
function identityError(message: string): Error & { code: string } {
|
|
12
|
+
return Object.assign(new Error(`PLATFORM_IDENTITY_INVALID: ${message}`), {
|
|
13
|
+
code: 'PLATFORM_IDENTITY_INVALID'
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isResourceIdentity(value: unknown): value is CanonicalResourceIdentity {
|
|
18
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
19
|
+
const candidate = value as Record<string, unknown>;
|
|
20
|
+
if (Object.keys(candidate).length !== 2 || typeof candidate.kind !== 'string') return false;
|
|
21
|
+
if (candidate.kind === 'number') {
|
|
22
|
+
return Number.isSafeInteger(candidate.value) && (candidate.value as number) > 0;
|
|
23
|
+
}
|
|
24
|
+
return (candidate.kind === 'id' || candidate.kind === 'key')
|
|
25
|
+
&& typeof candidate.value === 'string'
|
|
26
|
+
&& candidate.value.length > 0
|
|
27
|
+
&& candidate.value.trim() === candidate.value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseResourceIdentity(value: unknown, label = 'identity'): CanonicalResourceIdentity {
|
|
31
|
+
if (!isResourceIdentity(value)) throw identityError(`${label} must contain exactly one valid kind/value pair`);
|
|
32
|
+
if (value.kind === 'number') return { kind: 'number', value: value.value };
|
|
33
|
+
if (value.kind === 'id') return { kind: 'id', value: value.value };
|
|
34
|
+
return { kind: 'key', value: value.value };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function serializeResourceIdentity(identity: ResourceIdentity): string {
|
|
38
|
+
return JSON.stringify(parseResourceIdentity(identity));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resourceIdentityEquals(left: unknown, right: unknown): boolean {
|
|
42
|
+
return isResourceIdentity(left) && isResourceIdentity(right)
|
|
43
|
+
&& left.kind === right.kind && left.value === right.value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function primaryIdentityKind(
|
|
47
|
+
declaration: ProviderIdentityDeclaration | undefined,
|
|
48
|
+
resourceKind: PlatformResourceKind
|
|
49
|
+
): ResourceIdentityKind {
|
|
50
|
+
const kind = declaration?.[resourceKind];
|
|
51
|
+
if (kind !== 'id' && kind !== 'number' && kind !== 'key') {
|
|
52
|
+
throw identityError(`provider does not declare a primary identity for ${resourceKind}`);
|
|
53
|
+
}
|
|
54
|
+
return kind;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseResourceToken(
|
|
58
|
+
token: string,
|
|
59
|
+
resourceKind: PlatformResourceKind,
|
|
60
|
+
declaration: ProviderIdentityDeclaration | undefined
|
|
61
|
+
): ResourceIdentity {
|
|
62
|
+
if (typeof token !== 'string' || token.length === 0) throw identityError('resource token must be a non-empty string');
|
|
63
|
+
const kind = primaryIdentityKind(declaration, resourceKind);
|
|
64
|
+
if (kind === 'number') {
|
|
65
|
+
if (!/^[1-9]\d*$/u.test(token)) throw identityError(`${resourceKind} token must be a positive safe integer`);
|
|
66
|
+
const value = Number(token);
|
|
67
|
+
if (!Number.isSafeInteger(value) || value <= 0) throw identityError(`${resourceKind} token must be a positive safe integer`);
|
|
68
|
+
return { kind, value };
|
|
69
|
+
}
|
|
70
|
+
const value = token.trim();
|
|
71
|
+
if (!value) throw identityError(`${resourceKind} token must not be empty`);
|
|
72
|
+
return { kind, value };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function identityFromRemoteValue(
|
|
76
|
+
value: unknown,
|
|
77
|
+
resourceKind: PlatformResourceKind,
|
|
78
|
+
declaration: ProviderIdentityDeclaration | undefined
|
|
79
|
+
): ResourceIdentity {
|
|
80
|
+
const kind = primaryIdentityKind(declaration, resourceKind);
|
|
81
|
+
if (isResourceIdentity(value)) {
|
|
82
|
+
if (value.kind !== kind) throw identityError(`${resourceKind} response identity kind does not match provider declaration`);
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
if (kind === 'number') {
|
|
86
|
+
if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) return { kind, value };
|
|
87
|
+
if (typeof value === 'string' && /^[1-9]\d*$/u.test(value)) return parseResourceToken(value, resourceKind, declaration);
|
|
88
|
+
throw identityError(`${resourceKind} response does not contain a valid number identity`);
|
|
89
|
+
}
|
|
90
|
+
if (typeof value !== 'string' || !value.trim()) throw identityError(`${resourceKind} response does not contain a valid string identity`);
|
|
91
|
+
return { kind, value: value.trim() };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resourceIdentityNumber(identity: ResourceIdentity | null | undefined): number | null {
|
|
95
|
+
return identity?.kind === 'number' ? identity.value : null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function resourceIdentityString(identity: ResourceIdentity | null | undefined): string | null {
|
|
99
|
+
return identity && (identity.kind === 'id' || identity.kind === 'key') ? identity.value : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function reviewMarker(identity: ResourceIdentity): string {
|
|
103
|
+
const parsed = parseResourceIdentity(identity);
|
|
104
|
+
if (parsed.kind === 'number') return `pr${parsed.value}`;
|
|
105
|
+
const encoded = Buffer.from(serializeResourceIdentity(parsed), 'utf8').toString('base64url');
|
|
106
|
+
return `pr:${encoded}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export {
|
|
110
|
+
identityFromRemoteValue,
|
|
111
|
+
isResourceIdentity,
|
|
112
|
+
parseResourceIdentity,
|
|
113
|
+
parseResourceToken,
|
|
114
|
+
primaryIdentityKind,
|
|
115
|
+
resourceIdentityEquals,
|
|
116
|
+
resourceIdentityNumber,
|
|
117
|
+
resourceIdentityString,
|
|
118
|
+
reviewMarker,
|
|
119
|
+
serializeResourceIdentity
|
|
120
|
+
};
|
|
121
|
+
export type { CanonicalResourceIdentity, PlatformResourceKind, ProviderIdentityDeclaration, ResourceIdentity, ResourceIdentityKind };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ProviderIdentityDeclaration, ResourceIdentity } from './resource-identity.ts';
|
|
2
|
+
import { parseResourceIdentity } from './resource-identity.ts';
|
|
3
|
+
import { isLegacyCompatibilityEnabled, legacyCompatibilityError, VERSION } from '../version.ts';
|
|
4
|
+
import { parse as parseYaml } from 'yaml';
|
|
5
|
+
|
|
6
|
+
function taskIssueIdentity(
|
|
7
|
+
frontmatter: Record<string, string | number | boolean | null>,
|
|
8
|
+
_declaration?: ProviderIdentityDeclaration,
|
|
9
|
+
runtimeVersion = VERSION
|
|
10
|
+
): ResourceIdentity | null {
|
|
11
|
+
const serialized = frontmatter.platform_issue_identity;
|
|
12
|
+
if (typeof serialized === 'string' && serialized.trim()) {
|
|
13
|
+
try {
|
|
14
|
+
const decoded = parseYaml(serialized);
|
|
15
|
+
return parseResourceIdentity(
|
|
16
|
+
JSON.parse(typeof decoded === 'string' ? decoded : serialized),
|
|
17
|
+
'platform_issue_identity'
|
|
18
|
+
);
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const legacy = frontmatter.issue_number;
|
|
24
|
+
const number = typeof legacy === 'number' ? legacy : typeof legacy === 'string' && /^[1-9]\d*$/u.test(legacy) ? Number(legacy) : NaN;
|
|
25
|
+
// TODO(compat): Remove the numeric issue_number fallback before the first stable v1.0.0 release.
|
|
26
|
+
if (Number.isSafeInteger(number) && number > 0 && !isLegacyCompatibilityEnabled(runtimeVersion)) {
|
|
27
|
+
throw legacyCompatibilityError('issue_number', runtimeVersion);
|
|
28
|
+
}
|
|
29
|
+
return Number.isSafeInteger(number) && number > 0 ? { kind: 'number', value: number } : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function taskIssueIdentityError(error: unknown): { code: string; message: string } {
|
|
33
|
+
const value = error && typeof error === 'object' ? error as { code?: unknown } : {};
|
|
34
|
+
return {
|
|
35
|
+
code: typeof value.code === 'string' ? value.code : 'PLATFORM_IDENTITY_INVALID',
|
|
36
|
+
message: error instanceof Error ? error.message : String(error)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export { taskIssueIdentity, taskIssueIdentityError };
|
package/lib/platform/types.ts
CHANGED
|
@@ -23,15 +23,17 @@ type PlatformOperation = {
|
|
|
23
23
|
type CommentIdentity = {
|
|
24
24
|
kind: 'task' | 'artifact' | 'summary' | 'cancel';
|
|
25
25
|
marker: string;
|
|
26
|
-
ids: number
|
|
26
|
+
ids: Array<number | string>;
|
|
27
27
|
parts: number;
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
import type { ResourceIdentity } from './resource-identity.ts';
|
|
31
|
+
|
|
30
32
|
type PlatformResult = {
|
|
31
33
|
status: PlatformStatus;
|
|
32
34
|
changed: boolean;
|
|
33
35
|
platform: { type: string | null; repository: string | null; currentUser: string | null };
|
|
34
|
-
resource: { kind: 'repository' | 'issue' | 'pull-request'; number: number | null };
|
|
36
|
+
resource: { kind: 'repository' | 'issue' | 'pull-request'; number: number | null; identity?: ResourceIdentity };
|
|
35
37
|
capabilities: PlatformCapabilities;
|
|
36
38
|
operations: PlatformOperation[];
|
|
37
39
|
comment: CommentIdentity | null;
|
|
@@ -2,20 +2,15 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { execFileSync } from "node:child_process";
|
|
4
4
|
|
|
5
|
-
import { hasPlatformCapability } from "./adapters.ts";
|
|
6
5
|
import { inspectRequiredChecks } from "./pr-checks.ts";
|
|
7
|
-
import {
|
|
6
|
+
import { resolvePlatformProviderContext } from "./context.ts";
|
|
8
7
|
import { resolveReviewedHeadRelation } from "./merged-pr-equivalence.ts";
|
|
9
8
|
import { readPrDeliveryFact } from "../task/pr-delivery-fact.ts";
|
|
9
|
+
import { resourceIdentityNumber } from "./resource-identity.ts";
|
|
10
10
|
|
|
11
11
|
const CHECK_TYPE = "required-checks";
|
|
12
12
|
const SHA_PATTERN = /^[0-9a-f]{7,40}$/i;
|
|
13
13
|
|
|
14
|
-
function validPrNumber(value: any): any {
|
|
15
|
-
const parsed = Number(value);
|
|
16
|
-
return Number.isInteger(parsed) && parsed > 0;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
14
|
function readPrFlow(repoRoot: any): any {
|
|
20
15
|
try {
|
|
21
16
|
const config = JSON.parse(fs.readFileSync(path.join(repoRoot, ".agents", ".airc.json"), "utf8"));
|
|
@@ -40,8 +35,9 @@ export function evaluateRequiredChecks(context: any, shared: any): any {
|
|
|
40
35
|
return shared.failResult(CHECK_TYPE, factRead.error.message, "check_failed");
|
|
41
36
|
}
|
|
42
37
|
const fact = factRead.status === "valid" ? factRead.fact : null;
|
|
43
|
-
const
|
|
44
|
-
|
|
38
|
+
const prIdentity = fact?.state === "bound" ? fact.identity.resource : null;
|
|
39
|
+
const prNumber = resourceIdentityNumber(prIdentity);
|
|
40
|
+
if (prFlow === "disabled" || fact?.state === "skipped" || !prIdentity) {
|
|
45
41
|
return shared.passResult(CHECK_TYPE, "Skipped: required checks are not applicable to this task");
|
|
46
42
|
}
|
|
47
43
|
|
|
@@ -90,7 +86,7 @@ export function evaluateRequiredChecks(context: any, shared: any): any {
|
|
|
90
86
|
return shared.failResult(CHECK_TYPE, inspection.error?.message || `Required checks are ${state || "unavailable"}`, "check_failed");
|
|
91
87
|
}
|
|
92
88
|
|
|
93
|
-
export function check({ taskDir }: any, shared: any): any {
|
|
89
|
+
export async function check({ taskDir }: any, shared: any): Promise<any> {
|
|
94
90
|
const task = shared.loadTask(taskDir);
|
|
95
91
|
if (!task.ok) return shared.failResult(CHECK_TYPE, task.message);
|
|
96
92
|
const prFlow = readPrFlow(shared.repoRoot);
|
|
@@ -100,8 +96,10 @@ export function check({ taskDir }: any, shared: any): any {
|
|
|
100
96
|
if (prFlow === "disabled" || (factRead.status === "valid" && factRead.fact.state === "skipped") || !bound) {
|
|
101
97
|
return evaluateRequiredChecks({ metadata: task.metadata, localHead: null, inspection: null, prFlow }, shared);
|
|
102
98
|
}
|
|
103
|
-
const
|
|
104
|
-
|
|
99
|
+
const loaded = await resolvePlatformProviderContext({ cwd: shared.repoRoot });
|
|
100
|
+
const platform = loaded.ok ? loaded.value.context : loaded.context;
|
|
101
|
+
const supportsChecks = loaded.ok && Boolean(loaded.value.provider.checks?.inspectRequired);
|
|
102
|
+
if (!supportsChecks) {
|
|
105
103
|
return shared.blockedResult(
|
|
106
104
|
CHECK_TYPE,
|
|
107
105
|
`Platform '${platform.platform.type || "none"}' does not provide required-checks inspection`,
|
|
@@ -110,7 +108,7 @@ export function check({ taskDir }: any, shared: any): any {
|
|
|
110
108
|
}
|
|
111
109
|
const localHead = readHead(shared.repoRoot);
|
|
112
110
|
if (!localHead) return evaluateRequiredChecks({ metadata: task.metadata, localHead, inspection: null, prFlow }, shared);
|
|
113
|
-
const inspection = inspectRequiredChecks(task.metadata.id, { cwd: shared.repoRoot });
|
|
111
|
+
const inspection = await inspectRequiredChecks(task.metadata.id, { cwd: shared.repoRoot });
|
|
114
112
|
return evaluateRequiredChecks({
|
|
115
113
|
metadata: task.metadata,
|
|
116
114
|
localHead,
|