@fitlab-ai/agent-infra 0.11.4 → 0.11.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/internal-cli.js +0 -5
- package/dist/lib/decide.js +4 -41
- package/dist/lib/git/branch-worktree.js +45 -0
- package/dist/lib/internal/cli-route-inventory.js +0 -3
- package/dist/lib/internal/task-activity.js +7 -40
- package/dist/lib/internal/task-event.js +4 -37
- package/dist/lib/internal/task-finalization.js +9 -2
- package/dist/lib/internal/task-ledger.js +5 -43
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +1 -4
- package/dist/lib/internal/task-review.js +5 -37
- package/dist/lib/internal/task-verify.js +3 -47
- package/dist/lib/internal/task-warning.js +4 -40
- package/dist/lib/merge.js +62 -10
- package/dist/lib/platform/github-provider.js +9 -14
- package/dist/lib/platform/github-release-notes.js +80 -11
- package/dist/lib/platform/none-provider.js +1 -1
- package/dist/lib/platform/pr-summary.js +4 -23
- package/dist/lib/platform/provider-contract.d.ts +39 -16
- package/dist/lib/platform/provider-contract.js +1 -1
- package/dist/lib/platform/provider-validation.js +83 -13
- package/dist/lib/platform/release-notes.js +7 -8
- package/dist/lib/platform/verification-sync.js +12 -49
- package/dist/lib/sandbox/capture.js +1 -1
- package/dist/lib/sandbox/commands/create.js +46 -1
- package/dist/lib/sandbox/commands/enter.js +1 -1
- package/dist/lib/sandbox/commands/start.js +1 -1
- package/dist/lib/sandbox/config.js +10 -0
- package/dist/lib/sandbox/control/client.js +4 -1
- package/dist/lib/sandbox/control/executor.js +12 -4
- package/dist/lib/sandbox/control/lifecycle.js +18 -13
- package/dist/lib/sandbox/control/protocol.js +2 -1
- package/dist/lib/sandbox/removal.js +15 -8
- package/dist/lib/sandbox/task-resolver.js +62 -2
- package/dist/lib/task/activity-intent.js +1 -2
- package/dist/lib/task/artifact-operations.js +36 -2
- package/dist/lib/task/artifact-schema.js +7 -7
- package/dist/lib/task/control-authority.js +9 -166
- package/dist/lib/task/decision-intents.js +2 -4
- package/dist/lib/task/events.js +7 -28
- package/dist/lib/task/finalization-handoff.js +92 -0
- package/dist/lib/task/finalization.js +123 -12
- package/dist/lib/task/intermediate-cleanup.js +9 -148
- package/dist/lib/task/ledger-intents.js +3 -6
- package/dist/lib/task/lifecycle.js +50 -11
- package/dist/lib/task/local-artifact-finalization.js +13 -28
- package/dist/lib/task/resolve-ref.js +3 -3
- package/dist/lib/task/review-command.js +1 -2
- package/dist/lib/task/review-finalization.js +1 -2
- package/dist/lib/task/snapshot.js +6 -6
- package/dist/lib/task/verification-engine.js +16 -11
- package/dist/lib/task/verification.js +1 -13
- package/dist/lib/task/workflow-warning-intents.js +2 -4
- package/dist/lib/task/write.js +122 -3
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/decide.ts +5 -35
- package/lib/git/branch-worktree.ts +41 -0
- package/lib/internal/cli-route-inventory.ts +0 -2
- package/lib/internal/task-activity.ts +6 -34
- package/lib/internal/task-event.ts +4 -37
- package/lib/internal/task-finalization.ts +9 -2
- package/lib/internal/task-ledger.ts +5 -38
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-operation-registry.ts +1 -4
- package/lib/internal/task-review.ts +5 -31
- package/lib/internal/task-verify.ts +3 -39
- package/lib/internal/task-warning.ts +3 -32
- package/lib/merge.ts +59 -10
- package/lib/platform/github-provider.ts +11 -18
- package/lib/platform/github-release-notes.ts +89 -14
- package/lib/platform/none-provider.ts +1 -1
- package/lib/platform/pr-summary.ts +4 -25
- package/lib/platform/provider-contract.ts +38 -12
- package/lib/platform/provider-validation.ts +83 -12
- package/lib/platform/release-notes.ts +11 -12
- package/lib/platform/verification-sync.ts +13 -53
- package/lib/sandbox/capture.ts +1 -1
- package/lib/sandbox/commands/create.ts +52 -1
- package/lib/sandbox/commands/enter.ts +1 -1
- package/lib/sandbox/commands/start.ts +1 -1
- package/lib/sandbox/config.ts +12 -0
- package/lib/sandbox/control/client.ts +5 -1
- package/lib/sandbox/control/executor.ts +13 -4
- package/lib/sandbox/control/lifecycle.ts +22 -13
- package/lib/sandbox/control/protocol.ts +3 -2
- package/lib/sandbox/removal.ts +23 -7
- package/lib/sandbox/task-resolver.ts +51 -2
- package/lib/task/activity-intent.ts +1 -2
- package/lib/task/artifact-operations.ts +58 -2
- package/lib/task/artifact-schema.ts +9 -7
- package/lib/task/control-authority.ts +14 -188
- package/lib/task/decision-intents.ts +2 -4
- package/lib/task/events.ts +8 -27
- package/lib/task/finalization-handoff.ts +104 -0
- package/lib/task/finalization.ts +120 -12
- package/lib/task/intermediate-cleanup.ts +9 -125
- package/lib/task/ledger-intents.ts +3 -6
- package/lib/task/lifecycle.ts +45 -13
- package/lib/task/local-artifact-finalization.ts +16 -35
- package/lib/task/resolve-ref.ts +3 -3
- package/lib/task/review-command.ts +2 -5
- package/lib/task/review-finalization.ts +1 -4
- package/lib/task/snapshot.ts +6 -6
- package/lib/task/verification-engine.ts +20 -10
- package/lib/task/verification.ts +1 -15
- package/lib/task/workflow-warning-intents.ts +2 -4
- package/lib/task/write.ts +116 -5
- package/package.json +1 -1
- package/templates/.agents/rules/evidence-reporting.en.md +1 -2
- package/templates/.agents/rules/evidence-reporting.zh-CN.md +1 -2
- package/templates/.agents/skills/analyze-task/SKILL.en.md +11 -2
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -2
- package/templates/.agents/skills/archive-tasks/SKILL.en.md +2 -2
- package/templates/.agents/skills/archive-tasks/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +109 -6
- package/templates/.agents/skills/code-task/reference/report-template.en.md +6 -6
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +6 -6
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +1 -3
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +1 -3
- package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/create-release-note/SKILL.en.md +17 -11
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +17 -11
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-code/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-pr/config/verify.en.json +3 -1
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +3 -1
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +2 -2
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
- package/dist/lib/internal/task-override.js +0 -138
- package/dist/lib/task/artifact-recovery.js +0 -476
- package/dist/lib/task/artifact-repair-intent.js +0 -181
- package/dist/lib/task/guard-override.js +0 -31
- package/dist/lib/task/human-override.js +0 -1259
- package/lib/internal/task-override.ts +0 -144
- package/lib/task/artifact-recovery.ts +0 -572
- package/lib/task/artifact-repair-intent.ts +0 -227
- package/lib/task/guard-override.ts +0 -50
- package/lib/task/human-override.ts +0 -1522
package/lib/merge.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
3
4
|
import { removeDirRecursive } from './remove-dir.ts';
|
|
4
5
|
import { validateCurrentTaskContract } from './task/current-contract.ts';
|
|
5
6
|
|
|
@@ -107,7 +108,7 @@ function extractTitle(content: string): string | null {
|
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
function normalizeTaskRecord(taskDir: string, taskFile: string, dateParts: DateParts): TaskRecord {
|
|
110
|
-
const taskId = path.basename(taskDir);
|
|
111
|
+
const taskId = path.basename(taskDir) === 'local' ? path.basename(path.dirname(taskDir)) : path.basename(taskDir);
|
|
111
112
|
const content = fs.readFileSync(taskFile, 'utf8');
|
|
112
113
|
const completedAt = extractField(content, 'completed_at');
|
|
113
114
|
const updatedAt = extractField(content, 'updated_at');
|
|
@@ -155,7 +156,7 @@ function scanSourceTasks(sourceDir: string): TaskRecord[] {
|
|
|
155
156
|
continue;
|
|
156
157
|
}
|
|
157
158
|
|
|
158
|
-
const taskDir = path.join(dayDir, taskEntry.name);
|
|
159
|
+
const taskDir = path.join(dayDir, taskEntry.name, 'local');
|
|
159
160
|
const taskFile = path.join(taskDir, 'task.md');
|
|
160
161
|
if (!fs.existsSync(taskFile)) {
|
|
161
162
|
continue;
|
|
@@ -251,7 +252,7 @@ function collectArchiveEntries(archiveDir: string): ArchiveManifestEntry[] {
|
|
|
251
252
|
}
|
|
252
253
|
|
|
253
254
|
const taskDir = path.join(dayDir, taskEntry.name);
|
|
254
|
-
const taskFile = path.join(taskDir, 'task.md');
|
|
255
|
+
const taskFile = path.join(taskDir, 'local', 'task.md');
|
|
255
256
|
const relativePath = `${yearEntry.name}/${monthEntry.name}/${dayEntry.name}/${taskEntry.name}/`;
|
|
256
257
|
let title = taskEntry.name;
|
|
257
258
|
let type = 'unknown';
|
|
@@ -624,10 +625,12 @@ function validateSourceRoot(sourceRoot: string): void {
|
|
|
624
625
|
function validateTaskDirectory(
|
|
625
626
|
sourceRoot: string,
|
|
626
627
|
taskDir: string,
|
|
627
|
-
seenTaskIds: Map<string, string
|
|
628
|
+
seenTaskIds: Map<string, string>,
|
|
629
|
+
archived = false
|
|
628
630
|
): void {
|
|
629
631
|
const taskId = path.basename(taskDir);
|
|
630
|
-
const
|
|
632
|
+
const contentDir = archived ? path.join(taskDir, 'local') : taskDir;
|
|
633
|
+
const taskFile = path.join(contentDir, 'task.md');
|
|
631
634
|
const taskDirStat = sourcePathStat(sourceRoot, taskDir);
|
|
632
635
|
if (!taskDirStat || !taskDirStat.isDirectory()) {
|
|
633
636
|
invalidSource(`${sourceRelativePath(sourceRoot, taskDir)} must be a directory`);
|
|
@@ -646,6 +649,33 @@ function validateTaskDirectory(
|
|
|
646
649
|
invalidSource(`${sourceRelativePath(sourceRoot, taskFile)} id '${String(contract.metadata.id ?? 'missing')}' does not match directory '${taskId}'`);
|
|
647
650
|
}
|
|
648
651
|
validateTaskTree(sourceRoot, taskDir);
|
|
652
|
+
if (archived) {
|
|
653
|
+
for (const entry of fs.readdirSync(taskDir, { withFileTypes: true })) {
|
|
654
|
+
if (!['local', 'github', 'derived'].includes(entry.name) || !entry.isDirectory()) {
|
|
655
|
+
invalidSource(`${sourceRelativePath(sourceRoot, path.join(taskDir, entry.name))} is not an allowed archive source directory`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
validateArchiveSource(sourceRoot, contentDir);
|
|
659
|
+
const githubDir = path.join(taskDir, 'github');
|
|
660
|
+
if (fs.existsSync(githubDir)) {
|
|
661
|
+
for (const capture of fs.readdirSync(githubDir, { withFileTypes: true })) {
|
|
662
|
+
const captureDir = path.join(githubDir, capture.name);
|
|
663
|
+
if (!capture.isDirectory() || !/^capture-[A-Za-z0-9._-]+$/.test(capture.name) || !fs.existsSync(path.join(captureDir, 'capture.md'))) {
|
|
664
|
+
invalidSource(`${sourceRelativePath(sourceRoot, captureDir)} is not a valid GitHub capture`);
|
|
665
|
+
}
|
|
666
|
+
validateArchiveSource(sourceRoot, captureDir);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
const derivedDir = path.join(taskDir, 'derived');
|
|
670
|
+
if (fs.existsSync(derivedDir)) {
|
|
671
|
+
for (const view of fs.readdirSync(derivedDir, { withFileTypes: true })) {
|
|
672
|
+
const viewDir = path.join(derivedDir, view.name);
|
|
673
|
+
if (!view.isDirectory() || !fs.existsSync(path.join(viewDir, 'inputs.md'))) {
|
|
674
|
+
invalidSource(`${sourceRelativePath(sourceRoot, viewDir)} requires inputs.md`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
649
679
|
|
|
650
680
|
const previousPath = seenTaskIds.get(taskId);
|
|
651
681
|
if (previousPath) {
|
|
@@ -654,6 +684,26 @@ function validateTaskDirectory(
|
|
|
654
684
|
seenTaskIds.set(taskId, sourceRelativePath(sourceRoot, taskDir));
|
|
655
685
|
}
|
|
656
686
|
|
|
687
|
+
function validateArchiveSource(sourceRoot: string, sourceDir: string): void {
|
|
688
|
+
const hashFile = path.join(sourceDir, 'contents.sha256');
|
|
689
|
+
const hashStat = sourcePathStat(sourceRoot, hashFile);
|
|
690
|
+
if (!hashStat?.isFile()) invalidSource(`${sourceRelativePath(sourceRoot, sourceDir)} requires contents.sha256`);
|
|
691
|
+
const files: string[] = [];
|
|
692
|
+
const walk = (dir: string) => {
|
|
693
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
694
|
+
const target = path.join(dir, entry.name);
|
|
695
|
+
if (entry.name === 'manifest.md') invalidSource(`${sourceRelativePath(sourceRoot, target)} is forbidden inside a TASK`);
|
|
696
|
+
if (entry.isDirectory()) walk(target);
|
|
697
|
+
else if (entry.isFile() && target !== hashFile) files.push(target);
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
walk(sourceDir);
|
|
701
|
+
const expected = files.sort((a, b) => Buffer.compare(Buffer.from(path.relative(sourceDir, a).split(path.sep).join('/')), Buffer.from(path.relative(sourceDir, b).split(path.sep).join('/'))))
|
|
702
|
+
.map((file) => `${crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex')} ${path.relative(sourceDir, file).split(path.sep).join('/')}`);
|
|
703
|
+
const actual = fs.readFileSync(hashFile, 'utf8').trimEnd().split(/\r?\n/).filter(Boolean);
|
|
704
|
+
if (JSON.stringify(expected) !== JSON.stringify(actual)) invalidSource(`${sourceRelativePath(sourceRoot, hashFile)} does not match local files`);
|
|
705
|
+
}
|
|
706
|
+
|
|
657
707
|
function validateMutableSection(
|
|
658
708
|
sourceRoot: string,
|
|
659
709
|
section: MutableSection,
|
|
@@ -730,9 +780,6 @@ function validateArchiveSection(
|
|
|
730
780
|
const monthDir = monthPath;
|
|
731
781
|
for (const dayEntry of fs.readdirSync(monthDir, { withFileTypes: true })) {
|
|
732
782
|
const dayPath = path.join(monthDir, dayEntry.name);
|
|
733
|
-
if (allowManifest(monthDir, dayEntry)) {
|
|
734
|
-
continue;
|
|
735
|
-
}
|
|
736
783
|
const dayStat = sourcePathStat(sourceRoot, dayPath);
|
|
737
784
|
if (!dayStat?.isDirectory() || !/^\d{2}$/.test(dayEntry.name)) {
|
|
738
785
|
invalidSource(`${sourceRelativePath(sourceRoot, dayPath)} is not a DD directory or manifest.md`);
|
|
@@ -745,7 +792,7 @@ function validateArchiveSection(
|
|
|
745
792
|
if (!taskStat?.isDirectory() || !TASK_ID_RE.test(taskEntry.name)) {
|
|
746
793
|
invalidSource(`${sourceRelativePath(sourceRoot, taskPath)} is not a TASK-YYYYMMDD-HHMMSS directory`);
|
|
747
794
|
}
|
|
748
|
-
validateTaskDirectory(sourceRoot, taskPath, seenTaskIds);
|
|
795
|
+
validateTaskDirectory(sourceRoot, taskPath, seenTaskIds, true);
|
|
749
796
|
}
|
|
750
797
|
}
|
|
751
798
|
}
|
|
@@ -904,9 +951,10 @@ function mergeArchiveSection(sourceArchive: string, localArchive: string, report
|
|
|
904
951
|
continue;
|
|
905
952
|
}
|
|
906
953
|
|
|
954
|
+
const sourceTaskDir = path.dirname(task.taskDir);
|
|
907
955
|
const destinationDir = path.join(localArchive, task.relativePath);
|
|
908
956
|
fs.mkdirSync(path.dirname(destinationDir), { recursive: true });
|
|
909
|
-
fs.cpSync(
|
|
957
|
+
fs.cpSync(sourceTaskDir, destinationDir, { recursive: true });
|
|
910
958
|
recordArchive(report, 'copied', {
|
|
911
959
|
action: 'copied',
|
|
912
960
|
symbol: '✓',
|
|
@@ -1045,6 +1093,7 @@ async function cmdMerge(args: string[]): Promise<void> {
|
|
|
1045
1093
|
const report = createReport(resolvedSource, backupRootRelative);
|
|
1046
1094
|
detectSourceMode(resolvedSource);
|
|
1047
1095
|
validateSourceWorkspace(resolvedSource);
|
|
1096
|
+
validateArchiveSection(workspaceDir, new Map());
|
|
1048
1097
|
|
|
1049
1098
|
for (const section of ALL_SECTIONS) {
|
|
1050
1099
|
fs.mkdirSync(path.join(workspaceDir, section), { recursive: true });
|
|
@@ -923,30 +923,23 @@ function createGitHubOperations(client: GitHubClient): Pick<PlatformProvider, 'i
|
|
|
923
923
|
if (collected.status === 'failed' || collected.status === 'blocked' || !('pullRequests' in collected)) {
|
|
924
924
|
return { ok: false, error: collected.error || { code: 'RELEASE_NOTES_COLLECTION_FAILED', message: 'Release notes could not be collected', retryable: false } };
|
|
925
925
|
}
|
|
926
|
-
const mergedPullRequests = collected.pullRequests.map((item: any)
|
|
927
|
-
id: String(item.number), identity: { kind: 'number', value: Number(item.number) }, number: Number(item.number),
|
|
928
|
-
|
|
926
|
+
const mergedPullRequests = collected.pullRequests.map((item: any) => ({
|
|
927
|
+
id: String(item.number), identity: { kind: 'number' as const, value: Number(item.number) }, number: Number(item.number),
|
|
928
|
+
title: String(item.title || ''), body: String(item.body || ''),
|
|
929
929
|
mergedAt: new Date(String(item.mergedAt)).toISOString(), displayUrl: String(item.url || ''),
|
|
930
930
|
labels: Array.isArray(item.labels) ? item.labels.map((label: any) => String(label.name || label)) : [],
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
const closingIssues: IssueSnapshot[] = collected.pullRequests.flatMap((item: any) =>
|
|
934
|
-
Array.isArray(item.closingIssuesReferences) ? item.closingIssuesReferences.map((issue: any) => ({
|
|
931
|
+
author: item.author,
|
|
932
|
+
closingIssues: item.closingIssues.map((issue: any) => ({
|
|
935
933
|
id: String(issue.number), identity: { kind: 'number', value: Number(issue.number) }, number: Number(issue.number),
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
);
|
|
940
|
-
const actors = [...collected.authors.values()].flatMap((items) => items.map((item: any) => ({
|
|
941
|
-
...(item.login ? { id: String(item.login) } : {}), ...(item.name ? { name: String(item.name) } : {})
|
|
942
|
-
})));
|
|
934
|
+
title: String(issue.title || ''), displayUrl: String(issue.url || ''), author: issue.author
|
|
935
|
+
}))
|
|
936
|
+
}));
|
|
943
937
|
return {
|
|
944
938
|
ok: true,
|
|
945
939
|
value: {
|
|
946
|
-
history:
|
|
940
|
+
history: collected.history,
|
|
941
|
+
commits: collected.commits,
|
|
947
942
|
mergedPullRequests,
|
|
948
|
-
closingIssues,
|
|
949
|
-
actors
|
|
950
943
|
}
|
|
951
944
|
};
|
|
952
945
|
}
|
|
@@ -1116,7 +1109,7 @@ function createGitHubProvider(
|
|
|
1116
1109
|
): PlatformProvider {
|
|
1117
1110
|
return {
|
|
1118
1111
|
type: input.providerType,
|
|
1119
|
-
contractVersion:
|
|
1112
|
+
contractVersion: 2,
|
|
1120
1113
|
identity: { issue: 'number', 'pull-request': 'number', comment: 'number', release: 'key' },
|
|
1121
1114
|
...createGitHubOperations(client),
|
|
1122
1115
|
context: {
|
|
@@ -9,7 +9,6 @@ type GitHubActorInput = {
|
|
|
9
9
|
|
|
10
10
|
type ReleaseNoteActor = {
|
|
11
11
|
name: string;
|
|
12
|
-
email: string | null;
|
|
13
12
|
login: string | null;
|
|
14
13
|
bot: boolean;
|
|
15
14
|
resolution: 'platform-user' | 'platform-noreply' | 'unresolved';
|
|
@@ -27,13 +26,23 @@ function normalizeGitHubActor(actor: GitHubActorInput): ReleaseNoteActor {
|
|
|
27
26
|
const login = platformLogin || noReplyLogin;
|
|
28
27
|
return {
|
|
29
28
|
name,
|
|
30
|
-
email,
|
|
31
29
|
login,
|
|
32
30
|
bot: Boolean(login?.endsWith('[bot]')),
|
|
33
31
|
resolution: platformLogin ? 'platform-user' : noReplyLogin ? 'platform-noreply' : 'unresolved'
|
|
34
32
|
};
|
|
35
33
|
}
|
|
36
34
|
|
|
35
|
+
function normalizePlatformAuthor(author: { login?: string | null } | null | undefined): ReleaseNoteActor | null {
|
|
36
|
+
if (!author) return null;
|
|
37
|
+
const login = author.login?.trim().toLowerCase() || null;
|
|
38
|
+
return {
|
|
39
|
+
name: login || '',
|
|
40
|
+
login,
|
|
41
|
+
bot: Boolean(login?.endsWith('[bot]')),
|
|
42
|
+
resolution: login ? 'platform-user' : 'unresolved'
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
37
46
|
function failure(error: { code: string; message: string; retryable: boolean }) {
|
|
38
47
|
return {
|
|
39
48
|
status: error.retryable ? 'blocked' as const : 'failed' as const,
|
|
@@ -89,7 +98,7 @@ function fetchGitHubReleaseNoteData(
|
|
|
89
98
|
{ cwd: options.cwd }
|
|
90
99
|
);
|
|
91
100
|
if (!releases.ok) return failure(releases.error);
|
|
92
|
-
const history = [];
|
|
101
|
+
const history: Array<{ tag: string; body: string; url: string | null }> = [];
|
|
93
102
|
for (const item of releases.value.filter((entry) => !entry.isDraft && !entry.isPrerelease).slice(0, input.historyLimit)) {
|
|
94
103
|
const viewed = client.json<{ body?: string; url?: string }>(
|
|
95
104
|
['release', 'view', String(item.tagName), '--repo', input.repository, '--json', 'body,url'],
|
|
@@ -99,33 +108,99 @@ function fetchGitHubReleaseNoteData(
|
|
|
99
108
|
history.push({ tag: String(item.tagName), body: String(viewed.value.body || ''), url: viewed.value.url || null });
|
|
100
109
|
}
|
|
101
110
|
const prs = client.json<Array<Record<string, unknown>>>(
|
|
102
|
-
['pr', 'list', '--repo', input.repository, '--state', 'merged', '--base', input.branch, '--limit', '1000', '--json', 'number,title,body,url,mergedAt,labels,author
|
|
111
|
+
['pr', 'list', '--repo', input.repository, '--state', 'merged', '--base', input.branch, '--limit', '1000', '--json', 'number,title,body,url,mergedAt,labels,author'],
|
|
103
112
|
{ cwd: options.cwd }
|
|
104
113
|
);
|
|
105
114
|
if (!prs.ok) return failure(prs.error);
|
|
106
115
|
const authors = new Map<string, ReleaseNoteActor[]>();
|
|
116
|
+
const fromTime = Date.parse(input.fromTime);
|
|
117
|
+
const toTime = Date.parse(input.toTime);
|
|
118
|
+
const pullRequests = prs.value.filter((item) => {
|
|
119
|
+
const mergedAt = Date.parse(String(item.mergedAt || ''));
|
|
120
|
+
return Number.isFinite(mergedAt) && mergedAt > fromTime && mergedAt <= toTime;
|
|
121
|
+
});
|
|
122
|
+
const eligiblePullRequestNumbers = new Set(pullRequests.map((item) => Number(item.number)));
|
|
123
|
+
const pullRequestNumbersByCommit = new Map<string, number[]>();
|
|
107
124
|
for (const oid of input.commitOids) {
|
|
108
|
-
const query = `query($owner:String!,$name:String!,$oid:GitObjectID!){repository(owner:$owner,name:$name){object(oid:$oid){... on Commit{authors(first:100){nodes{name email user{login}} pageInfo{hasNextPage}}}}}}`;
|
|
125
|
+
const query = `query($owner:String!,$name:String!,$oid:GitObjectID!){repository(owner:$owner,name:$name){object(oid:$oid){... on Commit{authors(first:100){nodes{name email user{login}} pageInfo{hasNextPage}} associatedPullRequests(first:100){nodes{number} pageInfo{hasNextPage}}}}}}`;
|
|
109
126
|
const [owner, name] = input.repository.split('/');
|
|
110
127
|
const result = client.json<{
|
|
111
|
-
data?: { repository?: { object?: {
|
|
128
|
+
data?: { repository?: { object?: {
|
|
129
|
+
authors?: { nodes?: GitHubActorInput[]; pageInfo?: { hasNextPage?: boolean } };
|
|
130
|
+
associatedPullRequests?: {
|
|
131
|
+
nodes?: Array<{ number?: number | null }>;
|
|
132
|
+
pageInfo?: { hasNextPage?: boolean };
|
|
133
|
+
};
|
|
134
|
+
} } };
|
|
112
135
|
}>([
|
|
113
136
|
'api', 'graphql', '-f', `query=${query}`, '-F', `owner=${owner}`, '-F', `name=${name}`, '-F', `oid=${oid}`
|
|
114
137
|
], { cwd: options.cwd });
|
|
115
138
|
if (!result.ok) return failure(result.error);
|
|
116
|
-
const
|
|
139
|
+
const object = result.value.data?.repository?.object;
|
|
140
|
+
const connection = object?.authors;
|
|
117
141
|
if (connection?.pageInfo?.hasNextPage) {
|
|
118
142
|
return failure({ code: 'RELEASE_NOTES_AUTHORS_TRUNCATED', message: `Commit authors exceeded the supported page size for ${oid}`, retryable: false });
|
|
119
143
|
}
|
|
120
144
|
authors.set(oid, (connection?.nodes || []).map(normalizeGitHubActor));
|
|
145
|
+
const associatedPullRequests = object?.associatedPullRequests;
|
|
146
|
+
if (!associatedPullRequests || !Array.isArray(associatedPullRequests.nodes) || !associatedPullRequests.pageInfo || typeof associatedPullRequests.pageInfo.hasNextPage !== 'boolean') {
|
|
147
|
+
return failure({ code: 'INVALID_PLATFORM_RESPONSE', message: `Associated pull request data is incomplete for commit ${oid}`, retryable: false });
|
|
148
|
+
}
|
|
149
|
+
if (associatedPullRequests.pageInfo.hasNextPage) {
|
|
150
|
+
return failure({ code: 'RELEASE_NOTES_ASSOCIATED_PULL_REQUESTS_TRUNCATED', message: `Associated pull requests exceeded the supported page size for ${oid}`, retryable: false });
|
|
151
|
+
}
|
|
152
|
+
const numbers: number[] = [];
|
|
153
|
+
for (const association of associatedPullRequests.nodes) {
|
|
154
|
+
if (typeof association?.number !== 'number' || !Number.isSafeInteger(association.number)) {
|
|
155
|
+
return failure({ code: 'INVALID_PLATFORM_RESPONSE', message: `Associated pull request identity is incomplete for commit ${oid}`, retryable: false });
|
|
156
|
+
}
|
|
157
|
+
if (eligiblePullRequestNumbers.has(association.number)) numbers.push(association.number);
|
|
158
|
+
}
|
|
159
|
+
pullRequestNumbersByCommit.set(oid, [...new Set(numbers)]);
|
|
121
160
|
}
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
161
|
+
const resolvedPullRequests = [];
|
|
162
|
+
const [owner, name] = input.repository.split('/');
|
|
163
|
+
const closingIssuesQuery = 'query($owner:String!,$name:String!,$number:Int!,$cursor:String){repository(owner:$owner,name:$name){pullRequest(number:$number){closingIssuesReferences(first:100,after:$cursor){nodes{number title url author{login}} pageInfo{hasNextPage endCursor}}}}}';
|
|
164
|
+
for (const item of pullRequests) {
|
|
165
|
+
const closingIssues = [];
|
|
166
|
+
let cursor: string | null = null;
|
|
167
|
+
for (;;) {
|
|
168
|
+
const args = ['api', 'graphql', '-f', `query=${closingIssuesQuery}`, '-F', `owner=${owner}`, '-F', `name=${name}`, '-F', `number=${String(item.number)}`];
|
|
169
|
+
if (cursor) args.push('-F', `cursor=${cursor}`);
|
|
170
|
+
const response = client.json<{
|
|
171
|
+
data?: { repository?: { pullRequest?: { closingIssuesReferences?: {
|
|
172
|
+
nodes?: Array<{ number?: number; title?: string; url?: string; author?: { login?: string | null } | null }>;
|
|
173
|
+
pageInfo?: { hasNextPage?: boolean; endCursor?: string | null };
|
|
174
|
+
} } } };
|
|
175
|
+
}>(args, { cwd: options.cwd });
|
|
176
|
+
if (!response.ok) return failure(response.error);
|
|
177
|
+
const connection = response.value.data?.repository?.pullRequest?.closingIssuesReferences;
|
|
178
|
+
if (!connection || !Array.isArray(connection.nodes) || !connection.pageInfo || typeof connection.pageInfo.hasNextPage !== 'boolean') {
|
|
179
|
+
return failure({ code: 'INVALID_PLATFORM_RESPONSE', message: `Closing Issue data is incomplete for pull request ${String(item.number)}`, retryable: false });
|
|
180
|
+
}
|
|
181
|
+
closingIssues.push(...connection.nodes.map((issue) => ({
|
|
182
|
+
number: Number(issue.number), title: String(issue.title || ''), url: String(issue.url || ''),
|
|
183
|
+
author: normalizePlatformAuthor(issue.author)
|
|
184
|
+
})));
|
|
185
|
+
if (!connection.pageInfo.hasNextPage) break;
|
|
186
|
+
if (!connection.pageInfo.endCursor || connection.pageInfo.endCursor === cursor) {
|
|
187
|
+
return failure({ code: 'PAGINATION_INVALID', message: `Closing Issue pagination is incomplete for pull request ${String(item.number)}`, retryable: false });
|
|
188
|
+
}
|
|
189
|
+
cursor = connection.pageInfo.endCursor;
|
|
190
|
+
}
|
|
191
|
+
resolvedPullRequests.push({
|
|
192
|
+
...item,
|
|
193
|
+
author: normalizePlatformAuthor((item.author as { login?: string | null } | null) || null),
|
|
194
|
+
closingIssues
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
const commits = input.commitOids.map((sha) => ({
|
|
198
|
+
sha,
|
|
199
|
+
url: `https://github.com/${input.repository}/commit/${sha}`,
|
|
200
|
+
pullRequestNumbers: pullRequestNumbersByCommit.get(sha) || [],
|
|
201
|
+
authors: authors.get(sha) || []
|
|
202
|
+
}));
|
|
203
|
+
return { status: 'no-op' as const, changed: false, history, pullRequests: resolvedPullRequests, commits, error: null };
|
|
129
204
|
}
|
|
130
205
|
|
|
131
206
|
export { fetchGitHubReleaseNoteData, normalizeGitHubActor, publishGitHubReleaseNotes };
|
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
function createNoneProvider(input: PlatformProviderFactoryInput): PlatformProvider {
|
|
9
9
|
return {
|
|
10
10
|
type: input.providerType,
|
|
11
|
-
contractVersion:
|
|
11
|
+
contractVersion: 2,
|
|
12
12
|
context: {
|
|
13
13
|
async resolve(): Promise<ProviderResult<PlatformContextSnapshot>> {
|
|
14
14
|
return {
|
|
@@ -3,7 +3,6 @@ import path from 'node:path';
|
|
|
3
3
|
|
|
4
4
|
import { enumerateArtifacts } from '../task/artifacts.ts';
|
|
5
5
|
import { parseTypedTaskFrontmatter } from '../task/frontmatter.ts';
|
|
6
|
-
import { renderHumanOverrideAudit } from '../task/human-override.ts';
|
|
7
6
|
import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
8
7
|
import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
|
|
9
8
|
import { readPrDeliveryFact } from '../task/pr-delivery-fact.ts';
|
|
@@ -11,7 +10,7 @@ import type { PrDeliveryFact } from '../task/pr-delivery-fact.ts';
|
|
|
11
10
|
import { resolvePlatformProviderContext } from './context.ts';
|
|
12
11
|
import type { LoadedContext, PlatformClient } from './context.ts';
|
|
13
12
|
import { normalizeCommentContent } from './issue-comments.ts';
|
|
14
|
-
import { CONTROL_MARKER_PATTERN,
|
|
13
|
+
import { CONTROL_MARKER_PATTERN, sanitizeMarkdownDocument, splitDocumentPlaceholder } from './comment-safety.ts';
|
|
15
14
|
import { platformResult } from './types.ts';
|
|
16
15
|
import type { PlatformResult } from './types.ts';
|
|
17
16
|
import type { OperationWarning } from '../task/operation-outcome.ts';
|
|
@@ -74,25 +73,6 @@ function summaryMarker(taskId: string): string {
|
|
|
74
73
|
return `<!-- sync-pr:${taskId}:summary -->`;
|
|
75
74
|
}
|
|
76
75
|
|
|
77
|
-
function escapeSummaryAuditText(value: string): string {
|
|
78
|
-
return escapeHtmlText(value).replace(/\r\n?/g, '\n');
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function renderSummaryAuditLine(value: string): string {
|
|
82
|
-
const safe = escapeSummaryAuditText(value);
|
|
83
|
-
const longestRun = Math.max(0, ...(safe.match(/`+/g) || []).map((run) => run.length));
|
|
84
|
-
const delimiter = '`'.repeat(longestRun + 1);
|
|
85
|
-
return `${delimiter} ${safe} ${delimiter}`;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function renderSafeHumanOverrideAudit(value: string): string {
|
|
89
|
-
const normalized = value.trim().replace(/\r\n?/g, '\n');
|
|
90
|
-
if (!normalized) return '';
|
|
91
|
-
const [heading, ...rows] = normalized.split('\n');
|
|
92
|
-
const safeHeading = heading === '## Human Override Audit' ? heading : renderSummaryAuditLine(heading || '');
|
|
93
|
-
return [safeHeading, ...rows.map((row) => row ? renderSummaryAuditLine(row) : '')].join('\n');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
76
|
function isSafeSummaryEnvelope(value: string, taskId: string): boolean {
|
|
97
77
|
return value.includes(summaryMarker(taskId))
|
|
98
78
|
&& (value.match(/<!--\s*sync-pr:/gi) || []).length === 1
|
|
@@ -136,9 +116,8 @@ function platformError(error: { code: string; message: string; retryable?: boole
|
|
|
136
116
|
return { code: error.code, message: error.message, retryable: error.retryable ?? false };
|
|
137
117
|
}
|
|
138
118
|
|
|
139
|
-
function buildPullRequestSummary(taskId: string, body: string, headSha: string
|
|
140
|
-
|
|
141
|
-
return normalizeCommentContent(`${summaryMarker(taskId)}\n<!-- last-commit: ${headSha} -->\n\n${sections}\n`);
|
|
119
|
+
function buildPullRequestSummary(taskId: string, body: string, headSha: string): string {
|
|
120
|
+
return normalizeCommentContent(`${summaryMarker(taskId)}\n<!-- last-commit: ${headSha} -->\n\n${body.replace(/\s+$/, '')}\n`);
|
|
142
121
|
}
|
|
143
122
|
|
|
144
123
|
function reconcileSummaryComment(comments: SummaryComment[], taskId: string, desired: string):
|
|
@@ -643,7 +622,7 @@ async function syncPullRequestSummary(
|
|
|
643
622
|
`<!-- manual-validation-receipt: transaction=${manual.transactionId}; receipt=${manual.receiptDigest}; evidence=${manualEvidenceDigest}; head=${manual.prHeadSha} -->`
|
|
644
623
|
)
|
|
645
624
|
: replaced.value;
|
|
646
|
-
const desired = buildPullRequestSummary(resolved.taskId, renderedBody, initial.value.head.sha
|
|
625
|
+
const desired = buildPullRequestSummary(resolved.taskId, renderedBody, initial.value.head.sha);
|
|
647
626
|
if (!isSafeSummaryEnvelope(desired, resolved.taskId)) return fail('failed', context, {
|
|
648
627
|
code: 'PR_SUMMARY_RENDER_INVALID',
|
|
649
628
|
message: 'Summary contains an invalid or duplicated reserved control marker',
|
|
@@ -221,16 +221,39 @@ type RepositoryMetadataSnapshot = {
|
|
|
221
221
|
}>;
|
|
222
222
|
};
|
|
223
223
|
|
|
224
|
+
type ReleaseNoteAuthor = {
|
|
225
|
+
name: string;
|
|
226
|
+
login: string | null;
|
|
227
|
+
bot: boolean;
|
|
228
|
+
resolution: 'platform-user' | 'platform-noreply' | 'unresolved';
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
type ReleaseNoteIssue = {
|
|
232
|
+
id: string;
|
|
233
|
+
identity: ResourceIdentity;
|
|
234
|
+
number: number;
|
|
235
|
+
title: string;
|
|
236
|
+
displayUrl: string;
|
|
237
|
+
author: ReleaseNoteAuthor | null;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
type ReleaseNotePullRequest = {
|
|
241
|
+
id: string;
|
|
242
|
+
identity: ResourceIdentity;
|
|
243
|
+
number: number;
|
|
244
|
+
title: string;
|
|
245
|
+
body: string;
|
|
246
|
+
mergedAt: string;
|
|
247
|
+
displayUrl: string;
|
|
248
|
+
labels: string[];
|
|
249
|
+
author: ReleaseNoteAuthor | null;
|
|
250
|
+
closingIssues: ReleaseNoteIssue[];
|
|
251
|
+
};
|
|
252
|
+
|
|
224
253
|
type ReleaseNotesFacts = {
|
|
225
|
-
history: Array<{
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
authoredAt: string;
|
|
229
|
-
author: { id?: string; name?: string } | null;
|
|
230
|
-
}>;
|
|
231
|
-
mergedPullRequests: Array<ChangeRequestSnapshot>;
|
|
232
|
-
closingIssues: Array<IssueSnapshot>;
|
|
233
|
-
actors: Array<{ id?: string; name?: string }>;
|
|
254
|
+
history: Array<{ tag: string; body: string; url: string | null }>;
|
|
255
|
+
commits: Array<{ sha: string; url: string | null; pullRequestNumbers: number[]; authors: ReleaseNoteAuthor[] }>;
|
|
256
|
+
mergedPullRequests: ReleaseNotePullRequest[];
|
|
234
257
|
};
|
|
235
258
|
|
|
236
259
|
type MilestoneReconciliation = {
|
|
@@ -249,14 +272,14 @@ type VerificationRemoteFacts = {
|
|
|
249
272
|
|
|
250
273
|
type PlatformProviderFactoryInput = {
|
|
251
274
|
providerType: string;
|
|
252
|
-
contractVersion:
|
|
275
|
+
contractVersion: 2;
|
|
253
276
|
repositoryRoot: string;
|
|
254
277
|
config: Readonly<Record<string, JsonValue>>;
|
|
255
278
|
};
|
|
256
279
|
|
|
257
280
|
type PlatformProvider = {
|
|
258
281
|
type: string;
|
|
259
|
-
contractVersion:
|
|
282
|
+
contractVersion: 2;
|
|
260
283
|
identity?: ProviderIdentityDeclaration;
|
|
261
284
|
context: {
|
|
262
285
|
resolve(input: {
|
|
@@ -466,7 +489,7 @@ type PlatformProviderFactory = (
|
|
|
466
489
|
input: PlatformProviderFactoryInput
|
|
467
490
|
) => Promise<PlatformProvider>;
|
|
468
491
|
|
|
469
|
-
const PLATFORM_PROVIDER_CONTRACT_VERSION =
|
|
492
|
+
const PLATFORM_PROVIDER_CONTRACT_VERSION = 2 as const;
|
|
470
493
|
|
|
471
494
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
472
495
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -605,6 +628,9 @@ export type {
|
|
|
605
628
|
RequiredCheckSnapshot,
|
|
606
629
|
RepositoryMetadataSnapshot,
|
|
607
630
|
ReleaseNotesFacts,
|
|
631
|
+
ReleaseNoteAuthor,
|
|
632
|
+
ReleaseNoteIssue,
|
|
633
|
+
ReleaseNotePullRequest,
|
|
608
634
|
ResourceIdentity,
|
|
609
635
|
SecurityAlertKind,
|
|
610
636
|
SecurityAlertSnapshot,
|
|
@@ -16,6 +16,8 @@ import type {
|
|
|
16
16
|
PlatformError,
|
|
17
17
|
PlatformProvider,
|
|
18
18
|
ProviderResult,
|
|
19
|
+
ReleaseNoteAuthor,
|
|
20
|
+
ReleaseNoteIssue,
|
|
19
21
|
ReleaseNotesFacts,
|
|
20
22
|
ReleaseSnapshot,
|
|
21
23
|
RemoteCommentSnapshot,
|
|
@@ -109,6 +111,15 @@ function arrayValue(value: unknown, label: string): unknown[] {
|
|
|
109
111
|
return value;
|
|
110
112
|
}
|
|
111
113
|
|
|
114
|
+
function positiveNumberValue(value: unknown, label: string): number {
|
|
115
|
+
if (!Number.isSafeInteger(value) || Number(value) <= 0) throw new Error(`${label} must be a positive integer`);
|
|
116
|
+
return Number(value);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function stringArray(value: unknown, label: string): string[] {
|
|
120
|
+
return arrayValue(value, label).map((entry, index) => stringValue(entry, `${label}[${index}]`));
|
|
121
|
+
}
|
|
122
|
+
|
|
112
123
|
function stableUnique<T>(values: T[], key: (value: T) => string, label: string): T[] {
|
|
113
124
|
const seen = new Set<string>();
|
|
114
125
|
for (const value of values) {
|
|
@@ -119,6 +130,16 @@ function stableUnique<T>(values: T[], key: (value: T) => string, label: string):
|
|
|
119
130
|
return [...values].sort((left, right) => key(left).localeCompare(key(right)));
|
|
120
131
|
}
|
|
121
132
|
|
|
133
|
+
function uniqueInOrder<T>(values: T[], key: (value: T) => string, label: string): T[] {
|
|
134
|
+
const seen = new Set<string>();
|
|
135
|
+
for (const value of values) {
|
|
136
|
+
const identityKey = key(value);
|
|
137
|
+
if (seen.has(identityKey)) throw new Error(`${label} contains duplicate identities`);
|
|
138
|
+
seen.add(identityKey);
|
|
139
|
+
}
|
|
140
|
+
return values;
|
|
141
|
+
}
|
|
142
|
+
|
|
122
143
|
function declaredIdentityKind(
|
|
123
144
|
declaration: ProviderIdentityDeclaration | undefined,
|
|
124
145
|
resourceKind: 'issue' | 'pull-request' | 'comment' | 'release'
|
|
@@ -389,20 +410,70 @@ function validateMetadata(value: unknown): RepositoryMetadataSnapshot {
|
|
|
389
410
|
|
|
390
411
|
function validateReleaseNotes(value: unknown, declaration?: ProviderIdentityDeclaration): ReleaseNotesFacts {
|
|
391
412
|
const item = record(value, 'release notes');
|
|
392
|
-
exactKeys(item, ['history', '
|
|
413
|
+
exactKeys(item, ['history', 'commits', 'mergedPullRequests'], 'release notes');
|
|
414
|
+
const releaseAuthor = (raw: unknown, label: string): ReleaseNoteAuthor | null => {
|
|
415
|
+
if (raw === null) return null;
|
|
416
|
+
const entry = record(raw, label);
|
|
417
|
+
exactKeys(entry, ['name', 'login', 'bot', 'resolution'], label);
|
|
418
|
+
if (typeof entry.bot !== 'boolean') throw new Error(`${label}.bot must be boolean`);
|
|
419
|
+
if (!['platform-user', 'platform-noreply', 'unresolved'].includes(String(entry.resolution))) {
|
|
420
|
+
throw new Error(`${label}.resolution is invalid`);
|
|
421
|
+
}
|
|
422
|
+
return {
|
|
423
|
+
name: stringValue(entry.name, `${label}.name`, true),
|
|
424
|
+
login: nullableString(entry.login, `${label}.login`),
|
|
425
|
+
bot: entry.bot,
|
|
426
|
+
resolution: entry.resolution as ReleaseNoteAuthor['resolution']
|
|
427
|
+
};
|
|
428
|
+
};
|
|
429
|
+
const issue = (raw: unknown, label: string): ReleaseNoteIssue => {
|
|
430
|
+
const entry = record(raw, label);
|
|
431
|
+
exactKeys(entry, ['id', 'identity', 'number', 'title', 'displayUrl', 'author'], label);
|
|
432
|
+
return {
|
|
433
|
+
id: stringValue(entry.id, `${label}.id`),
|
|
434
|
+
identity: identity(entry.identity, `${label}.identity`, declaredIdentityKind(declaration, 'issue')),
|
|
435
|
+
number: positiveNumberValue(entry.number, `${label}.number`),
|
|
436
|
+
title: stringValue(entry.title, `${label}.title`, true),
|
|
437
|
+
displayUrl: stringValue(entry.displayUrl, `${label}.displayUrl`, true),
|
|
438
|
+
author: releaseAuthor(entry.author, `${label}.author`)
|
|
439
|
+
};
|
|
440
|
+
};
|
|
393
441
|
return {
|
|
394
|
-
history:
|
|
442
|
+
history: uniqueInOrder(arrayValue(item.history, 'release notes.history').map((raw) => {
|
|
395
443
|
const entry = record(raw, 'release notes.history[]');
|
|
396
|
-
exactKeys(entry, ['
|
|
397
|
-
return {
|
|
398
|
-
}), (entry) => entry.
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
444
|
+
exactKeys(entry, ['tag', 'body', 'url'], 'release notes.history[]');
|
|
445
|
+
return { tag: stringValue(entry.tag, 'history.tag'), body: stringValue(entry.body, 'history.body', true), url: nullableString(entry.url, 'history.url') };
|
|
446
|
+
}), (entry) => entry.tag, 'release notes.history'),
|
|
447
|
+
commits: stableUnique(arrayValue(item.commits, 'release notes.commits').map((raw) => {
|
|
448
|
+
const entry = record(raw, 'release notes.commits[]');
|
|
449
|
+
exactKeys(entry, ['sha', 'url', 'pullRequestNumbers', 'authors'], 'release notes.commits[]');
|
|
450
|
+
return {
|
|
451
|
+
sha: stringValue(entry.sha, 'commits.sha'),
|
|
452
|
+
url: nullableString(entry.url, 'commits.url'),
|
|
453
|
+
pullRequestNumbers: stableUnique(arrayValue(entry.pullRequestNumbers, 'commits.pullRequestNumbers').map((number) => positiveNumberValue(number, 'commits.pullRequestNumbers[]')), (number) => String(number), 'commits.pullRequestNumbers'),
|
|
454
|
+
authors: arrayValue(entry.authors, 'commits.authors').map((rawAuthor) => {
|
|
455
|
+
const normalized = releaseAuthor(rawAuthor, 'commits.author');
|
|
456
|
+
if (!normalized) throw new Error('commits.author must identify an author');
|
|
457
|
+
return normalized;
|
|
458
|
+
})
|
|
459
|
+
};
|
|
460
|
+
}), (entry) => entry.sha, 'release notes.commits'),
|
|
461
|
+
mergedPullRequests: stableUnique(arrayValue(item.mergedPullRequests, 'release notes.mergedPullRequests').map((raw) => {
|
|
462
|
+
const entry = record(raw, 'release notes.mergedPullRequests[]');
|
|
463
|
+
exactKeys(entry, ['id', 'identity', 'number', 'title', 'body', 'mergedAt', 'displayUrl', 'labels', 'author', 'closingIssues'], 'release notes.mergedPullRequests[]');
|
|
464
|
+
return {
|
|
465
|
+
id: stringValue(entry.id, 'pullRequest.id'),
|
|
466
|
+
identity: identity(entry.identity, 'pullRequest.identity', declaredIdentityKind(declaration, 'pull-request')),
|
|
467
|
+
number: positiveNumberValue(entry.number, 'pullRequest.number'),
|
|
468
|
+
title: stringValue(entry.title, 'pullRequest.title', true),
|
|
469
|
+
body: stringValue(entry.body, 'pullRequest.body', true),
|
|
470
|
+
mergedAt: utcTimestamp(entry.mergedAt, 'pullRequest.mergedAt'),
|
|
471
|
+
displayUrl: stringValue(entry.displayUrl, 'pullRequest.displayUrl', true),
|
|
472
|
+
labels: stringArray(entry.labels, 'pullRequest.labels'),
|
|
473
|
+
author: releaseAuthor(entry.author, 'pullRequest.author'),
|
|
474
|
+
closingIssues: stableUnique(arrayValue(entry.closingIssues, 'pullRequest.closingIssues').map((entry) => issue(entry, 'pullRequest.closingIssues[]')), (entry) => serializeResourceIdentity(entry.identity), 'pullRequest.closingIssues')
|
|
475
|
+
};
|
|
476
|
+
}), (entry) => serializeResourceIdentity(entry.identity), 'release notes.mergedPullRequests')
|
|
406
477
|
};
|
|
407
478
|
}
|
|
408
479
|
|