@fitlab-ai/agent-infra 0.11.5 → 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/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/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/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/lifecycle.js +18 -13
- package/dist/lib/sandbox/task-resolver.js +62 -2
- package/dist/lib/task/resolve-ref.js +3 -3
- package/dist/lib/task/write.js +121 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- 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/provider-contract.ts +38 -12
- package/lib/platform/provider-validation.ts +83 -12
- package/lib/platform/release-notes.ts +11 -12
- 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/lifecycle.ts +22 -13
- package/lib/sandbox/task-resolver.ts +51 -2
- package/lib/task/resolve-ref.ts +3 -3
- package/lib/task/write.ts +115 -1
- 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/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/create-release-note/SKILL.en.md +17 -11
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +17 -11
package/dist/lib/merge.js
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.js';
|
|
4
5
|
import { validateCurrentTaskContract } from './task/current-contract.js';
|
|
5
6
|
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
@@ -44,7 +45,7 @@ function extractTitle(content) {
|
|
|
44
45
|
.replace(/\|/g, '\\|') || null;
|
|
45
46
|
}
|
|
46
47
|
function normalizeTaskRecord(taskDir, taskFile, dateParts) {
|
|
47
|
-
const taskId = path.basename(taskDir);
|
|
48
|
+
const taskId = path.basename(taskDir) === 'local' ? path.basename(path.dirname(taskDir)) : path.basename(taskDir);
|
|
48
49
|
const content = fs.readFileSync(taskFile, 'utf8');
|
|
49
50
|
const completedAt = extractField(content, 'completed_at');
|
|
50
51
|
const updatedAt = extractField(content, 'updated_at');
|
|
@@ -85,7 +86,7 @@ function scanSourceTasks(sourceDir) {
|
|
|
85
86
|
if (!taskEntry.isDirectory() || !TASK_ID_RE.test(taskEntry.name)) {
|
|
86
87
|
continue;
|
|
87
88
|
}
|
|
88
|
-
const taskDir = path.join(dayDir, taskEntry.name);
|
|
89
|
+
const taskDir = path.join(dayDir, taskEntry.name, 'local');
|
|
89
90
|
const taskFile = path.join(taskDir, 'task.md');
|
|
90
91
|
if (!fs.existsSync(taskFile)) {
|
|
91
92
|
continue;
|
|
@@ -165,7 +166,7 @@ function collectArchiveEntries(archiveDir) {
|
|
|
165
166
|
continue;
|
|
166
167
|
}
|
|
167
168
|
const taskDir = path.join(dayDir, taskEntry.name);
|
|
168
|
-
const taskFile = path.join(taskDir, 'task.md');
|
|
169
|
+
const taskFile = path.join(taskDir, 'local', 'task.md');
|
|
169
170
|
const relativePath = `${yearEntry.name}/${monthEntry.name}/${dayEntry.name}/${taskEntry.name}/`;
|
|
170
171
|
let title = taskEntry.name;
|
|
171
172
|
let type = 'unknown';
|
|
@@ -471,9 +472,10 @@ function validateSourceRoot(sourceRoot) {
|
|
|
471
472
|
invalidSource('source path must be a real directory');
|
|
472
473
|
}
|
|
473
474
|
}
|
|
474
|
-
function validateTaskDirectory(sourceRoot, taskDir, seenTaskIds) {
|
|
475
|
+
function validateTaskDirectory(sourceRoot, taskDir, seenTaskIds, archived = false) {
|
|
475
476
|
const taskId = path.basename(taskDir);
|
|
476
|
-
const
|
|
477
|
+
const contentDir = archived ? path.join(taskDir, 'local') : taskDir;
|
|
478
|
+
const taskFile = path.join(contentDir, 'task.md');
|
|
477
479
|
const taskDirStat = sourcePathStat(sourceRoot, taskDir);
|
|
478
480
|
if (!taskDirStat || !taskDirStat.isDirectory()) {
|
|
479
481
|
invalidSource(`${sourceRelativePath(sourceRoot, taskDir)} must be a directory`);
|
|
@@ -491,12 +493,63 @@ function validateTaskDirectory(sourceRoot, taskDir, seenTaskIds) {
|
|
|
491
493
|
invalidSource(`${sourceRelativePath(sourceRoot, taskFile)} id '${String(contract.metadata.id ?? 'missing')}' does not match directory '${taskId}'`);
|
|
492
494
|
}
|
|
493
495
|
validateTaskTree(sourceRoot, taskDir);
|
|
496
|
+
if (archived) {
|
|
497
|
+
for (const entry of fs.readdirSync(taskDir, { withFileTypes: true })) {
|
|
498
|
+
if (!['local', 'github', 'derived'].includes(entry.name) || !entry.isDirectory()) {
|
|
499
|
+
invalidSource(`${sourceRelativePath(sourceRoot, path.join(taskDir, entry.name))} is not an allowed archive source directory`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
validateArchiveSource(sourceRoot, contentDir);
|
|
503
|
+
const githubDir = path.join(taskDir, 'github');
|
|
504
|
+
if (fs.existsSync(githubDir)) {
|
|
505
|
+
for (const capture of fs.readdirSync(githubDir, { withFileTypes: true })) {
|
|
506
|
+
const captureDir = path.join(githubDir, capture.name);
|
|
507
|
+
if (!capture.isDirectory() || !/^capture-[A-Za-z0-9._-]+$/.test(capture.name) || !fs.existsSync(path.join(captureDir, 'capture.md'))) {
|
|
508
|
+
invalidSource(`${sourceRelativePath(sourceRoot, captureDir)} is not a valid GitHub capture`);
|
|
509
|
+
}
|
|
510
|
+
validateArchiveSource(sourceRoot, captureDir);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const derivedDir = path.join(taskDir, 'derived');
|
|
514
|
+
if (fs.existsSync(derivedDir)) {
|
|
515
|
+
for (const view of fs.readdirSync(derivedDir, { withFileTypes: true })) {
|
|
516
|
+
const viewDir = path.join(derivedDir, view.name);
|
|
517
|
+
if (!view.isDirectory() || !fs.existsSync(path.join(viewDir, 'inputs.md'))) {
|
|
518
|
+
invalidSource(`${sourceRelativePath(sourceRoot, viewDir)} requires inputs.md`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
494
523
|
const previousPath = seenTaskIds.get(taskId);
|
|
495
524
|
if (previousPath) {
|
|
496
525
|
invalidSource(`duplicate task ID ${taskId} at ${previousPath} and ${sourceRelativePath(sourceRoot, taskDir)}`);
|
|
497
526
|
}
|
|
498
527
|
seenTaskIds.set(taskId, sourceRelativePath(sourceRoot, taskDir));
|
|
499
528
|
}
|
|
529
|
+
function validateArchiveSource(sourceRoot, sourceDir) {
|
|
530
|
+
const hashFile = path.join(sourceDir, 'contents.sha256');
|
|
531
|
+
const hashStat = sourcePathStat(sourceRoot, hashFile);
|
|
532
|
+
if (!hashStat?.isFile())
|
|
533
|
+
invalidSource(`${sourceRelativePath(sourceRoot, sourceDir)} requires contents.sha256`);
|
|
534
|
+
const files = [];
|
|
535
|
+
const walk = (dir) => {
|
|
536
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
537
|
+
const target = path.join(dir, entry.name);
|
|
538
|
+
if (entry.name === 'manifest.md')
|
|
539
|
+
invalidSource(`${sourceRelativePath(sourceRoot, target)} is forbidden inside a TASK`);
|
|
540
|
+
if (entry.isDirectory())
|
|
541
|
+
walk(target);
|
|
542
|
+
else if (entry.isFile() && target !== hashFile)
|
|
543
|
+
files.push(target);
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
walk(sourceDir);
|
|
547
|
+
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('/'))))
|
|
548
|
+
.map((file) => `${crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex')} ${path.relative(sourceDir, file).split(path.sep).join('/')}`);
|
|
549
|
+
const actual = fs.readFileSync(hashFile, 'utf8').trimEnd().split(/\r?\n/).filter(Boolean);
|
|
550
|
+
if (JSON.stringify(expected) !== JSON.stringify(actual))
|
|
551
|
+
invalidSource(`${sourceRelativePath(sourceRoot, hashFile)} does not match local files`);
|
|
552
|
+
}
|
|
500
553
|
function validateMutableSection(sourceRoot, section, seenTaskIds) {
|
|
501
554
|
const sectionDir = path.join(sourceRoot, section);
|
|
502
555
|
const sectionStat = sourcePathStat(sourceRoot, sectionDir);
|
|
@@ -561,9 +614,6 @@ function validateArchiveSection(sourceRoot, seenTaskIds) {
|
|
|
561
614
|
const monthDir = monthPath;
|
|
562
615
|
for (const dayEntry of fs.readdirSync(monthDir, { withFileTypes: true })) {
|
|
563
616
|
const dayPath = path.join(monthDir, dayEntry.name);
|
|
564
|
-
if (allowManifest(monthDir, dayEntry)) {
|
|
565
|
-
continue;
|
|
566
|
-
}
|
|
567
617
|
const dayStat = sourcePathStat(sourceRoot, dayPath);
|
|
568
618
|
if (!dayStat?.isDirectory() || !/^\d{2}$/.test(dayEntry.name)) {
|
|
569
619
|
invalidSource(`${sourceRelativePath(sourceRoot, dayPath)} is not a DD directory or manifest.md`);
|
|
@@ -575,7 +625,7 @@ function validateArchiveSection(sourceRoot, seenTaskIds) {
|
|
|
575
625
|
if (!taskStat?.isDirectory() || !TASK_ID_RE.test(taskEntry.name)) {
|
|
576
626
|
invalidSource(`${sourceRelativePath(sourceRoot, taskPath)} is not a TASK-YYYYMMDD-HHMMSS directory`);
|
|
577
627
|
}
|
|
578
|
-
validateTaskDirectory(sourceRoot, taskPath, seenTaskIds);
|
|
628
|
+
validateTaskDirectory(sourceRoot, taskPath, seenTaskIds, true);
|
|
579
629
|
}
|
|
580
630
|
}
|
|
581
631
|
}
|
|
@@ -716,9 +766,10 @@ function mergeArchiveSection(sourceArchive, localArchive, report) {
|
|
|
716
766
|
});
|
|
717
767
|
continue;
|
|
718
768
|
}
|
|
769
|
+
const sourceTaskDir = path.dirname(task.taskDir);
|
|
719
770
|
const destinationDir = path.join(localArchive, task.relativePath);
|
|
720
771
|
fs.mkdirSync(path.dirname(destinationDir), { recursive: true });
|
|
721
|
-
fs.cpSync(
|
|
772
|
+
fs.cpSync(sourceTaskDir, destinationDir, { recursive: true });
|
|
722
773
|
recordArchive(report, 'copied', {
|
|
723
774
|
action: 'copied',
|
|
724
775
|
symbol: '✓',
|
|
@@ -829,6 +880,7 @@ async function cmdMerge(args) {
|
|
|
829
880
|
const report = createReport(resolvedSource, backupRootRelative);
|
|
830
881
|
detectSourceMode(resolvedSource);
|
|
831
882
|
validateSourceWorkspace(resolvedSource);
|
|
883
|
+
validateArchiveSection(workspaceDir, new Map());
|
|
832
884
|
for (const section of ALL_SECTIONS) {
|
|
833
885
|
fs.mkdirSync(path.join(workspaceDir, section), { recursive: true });
|
|
834
886
|
}
|
|
@@ -921,26 +921,21 @@ function createGitHubOperations(client) {
|
|
|
921
921
|
}
|
|
922
922
|
const mergedPullRequests = collected.pullRequests.map((item) => ({
|
|
923
923
|
id: String(item.number), identity: { kind: 'number', value: Number(item.number) }, number: Number(item.number),
|
|
924
|
-
|
|
924
|
+
title: String(item.title || ''), body: String(item.body || ''),
|
|
925
925
|
mergedAt: new Date(String(item.mergedAt)).toISOString(), displayUrl: String(item.url || ''),
|
|
926
926
|
labels: Array.isArray(item.labels) ? item.labels.map((label) => String(label.name || label)) : [],
|
|
927
|
-
|
|
927
|
+
author: item.author,
|
|
928
|
+
closingIssues: item.closingIssues.map((issue) => ({
|
|
929
|
+
id: String(issue.number), identity: { kind: 'number', value: Number(issue.number) }, number: Number(issue.number),
|
|
930
|
+
title: String(issue.title || ''), displayUrl: String(issue.url || ''), author: issue.author
|
|
931
|
+
}))
|
|
928
932
|
}));
|
|
929
|
-
const closingIssues = collected.pullRequests.flatMap((item) => Array.isArray(item.closingIssuesReferences) ? item.closingIssuesReferences.map((issue) => ({
|
|
930
|
-
id: String(issue.number), identity: { kind: 'number', value: Number(issue.number) }, number: Number(issue.number),
|
|
931
|
-
state: 'closed', title: String(issue.title || ''), body: '', labels: Array.isArray(issue.labels) ? issue.labels.map((label) => String(label.name || label)) : [],
|
|
932
|
-
assignees: [], milestone: null, fields: {}, displayUrl: String(issue.url || '')
|
|
933
|
-
})) : []);
|
|
934
|
-
const actors = [...collected.authors.values()].flatMap((items) => items.map((item) => ({
|
|
935
|
-
...(item.login ? { id: String(item.login) } : {}), ...(item.name ? { name: String(item.name) } : {})
|
|
936
|
-
})));
|
|
937
933
|
return {
|
|
938
934
|
ok: true,
|
|
939
935
|
value: {
|
|
940
|
-
history:
|
|
936
|
+
history: collected.history,
|
|
937
|
+
commits: collected.commits,
|
|
941
938
|
mergedPullRequests,
|
|
942
|
-
closingIssues,
|
|
943
|
-
actors
|
|
944
939
|
}
|
|
945
940
|
};
|
|
946
941
|
}
|
|
@@ -1121,7 +1116,7 @@ function createGitHubOperations(client) {
|
|
|
1121
1116
|
function createGitHubProvider(input, client = createGitHubClient()) {
|
|
1122
1117
|
return {
|
|
1123
1118
|
type: input.providerType,
|
|
1124
|
-
contractVersion:
|
|
1119
|
+
contractVersion: 2,
|
|
1125
1120
|
identity: { issue: 'number', 'pull-request': 'number', comment: 'number', release: 'key' },
|
|
1126
1121
|
...createGitHubOperations(client),
|
|
1127
1122
|
context: {
|
|
@@ -9,12 +9,22 @@ function normalizeGitHubActor(actor) {
|
|
|
9
9
|
const login = platformLogin || noReplyLogin;
|
|
10
10
|
return {
|
|
11
11
|
name,
|
|
12
|
-
email,
|
|
13
12
|
login,
|
|
14
13
|
bot: Boolean(login?.endsWith('[bot]')),
|
|
15
14
|
resolution: platformLogin ? 'platform-user' : noReplyLogin ? 'platform-noreply' : 'unresolved'
|
|
16
15
|
};
|
|
17
16
|
}
|
|
17
|
+
function normalizePlatformAuthor(author) {
|
|
18
|
+
if (!author)
|
|
19
|
+
return null;
|
|
20
|
+
const login = author.login?.trim().toLowerCase() || null;
|
|
21
|
+
return {
|
|
22
|
+
name: login || '',
|
|
23
|
+
login,
|
|
24
|
+
bot: Boolean(login?.endsWith('[bot]')),
|
|
25
|
+
resolution: login ? 'platform-user' : 'unresolved'
|
|
26
|
+
};
|
|
27
|
+
}
|
|
18
28
|
function failure(error) {
|
|
19
29
|
return {
|
|
20
30
|
status: error.retryable ? 'blocked' : 'failed',
|
|
@@ -66,31 +76,90 @@ function fetchGitHubReleaseNoteData(input, options = {}) {
|
|
|
66
76
|
return failure(viewed.error);
|
|
67
77
|
history.push({ tag: String(item.tagName), body: String(viewed.value.body || ''), url: viewed.value.url || null });
|
|
68
78
|
}
|
|
69
|
-
const prs = client.json(['pr', 'list', '--repo', input.repository, '--state', 'merged', '--base', input.branch, '--limit', '1000', '--json', 'number,title,body,url,mergedAt,labels,author
|
|
79
|
+
const prs = client.json(['pr', 'list', '--repo', input.repository, '--state', 'merged', '--base', input.branch, '--limit', '1000', '--json', 'number,title,body,url,mergedAt,labels,author'], { cwd: options.cwd });
|
|
70
80
|
if (!prs.ok)
|
|
71
81
|
return failure(prs.error);
|
|
72
82
|
const authors = new Map();
|
|
83
|
+
const fromTime = Date.parse(input.fromTime);
|
|
84
|
+
const toTime = Date.parse(input.toTime);
|
|
85
|
+
const pullRequests = prs.value.filter((item) => {
|
|
86
|
+
const mergedAt = Date.parse(String(item.mergedAt || ''));
|
|
87
|
+
return Number.isFinite(mergedAt) && mergedAt > fromTime && mergedAt <= toTime;
|
|
88
|
+
});
|
|
89
|
+
const eligiblePullRequestNumbers = new Set(pullRequests.map((item) => Number(item.number)));
|
|
90
|
+
const pullRequestNumbersByCommit = new Map();
|
|
73
91
|
for (const oid of input.commitOids) {
|
|
74
|
-
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}}}}}}`;
|
|
92
|
+
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}}}}}}`;
|
|
75
93
|
const [owner, name] = input.repository.split('/');
|
|
76
94
|
const result = client.json([
|
|
77
95
|
'api', 'graphql', '-f', `query=${query}`, '-F', `owner=${owner}`, '-F', `name=${name}`, '-F', `oid=${oid}`
|
|
78
96
|
], { cwd: options.cwd });
|
|
79
97
|
if (!result.ok)
|
|
80
98
|
return failure(result.error);
|
|
81
|
-
const
|
|
99
|
+
const object = result.value.data?.repository?.object;
|
|
100
|
+
const connection = object?.authors;
|
|
82
101
|
if (connection?.pageInfo?.hasNextPage) {
|
|
83
102
|
return failure({ code: 'RELEASE_NOTES_AUTHORS_TRUNCATED', message: `Commit authors exceeded the supported page size for ${oid}`, retryable: false });
|
|
84
103
|
}
|
|
85
104
|
authors.set(oid, (connection?.nodes || []).map(normalizeGitHubActor));
|
|
105
|
+
const associatedPullRequests = object?.associatedPullRequests;
|
|
106
|
+
if (!associatedPullRequests || !Array.isArray(associatedPullRequests.nodes) || !associatedPullRequests.pageInfo || typeof associatedPullRequests.pageInfo.hasNextPage !== 'boolean') {
|
|
107
|
+
return failure({ code: 'INVALID_PLATFORM_RESPONSE', message: `Associated pull request data is incomplete for commit ${oid}`, retryable: false });
|
|
108
|
+
}
|
|
109
|
+
if (associatedPullRequests.pageInfo.hasNextPage) {
|
|
110
|
+
return failure({ code: 'RELEASE_NOTES_ASSOCIATED_PULL_REQUESTS_TRUNCATED', message: `Associated pull requests exceeded the supported page size for ${oid}`, retryable: false });
|
|
111
|
+
}
|
|
112
|
+
const numbers = [];
|
|
113
|
+
for (const association of associatedPullRequests.nodes) {
|
|
114
|
+
if (typeof association?.number !== 'number' || !Number.isSafeInteger(association.number)) {
|
|
115
|
+
return failure({ code: 'INVALID_PLATFORM_RESPONSE', message: `Associated pull request identity is incomplete for commit ${oid}`, retryable: false });
|
|
116
|
+
}
|
|
117
|
+
if (eligiblePullRequestNumbers.has(association.number))
|
|
118
|
+
numbers.push(association.number);
|
|
119
|
+
}
|
|
120
|
+
pullRequestNumbersByCommit.set(oid, [...new Set(numbers)]);
|
|
86
121
|
}
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
122
|
+
const resolvedPullRequests = [];
|
|
123
|
+
const [owner, name] = input.repository.split('/');
|
|
124
|
+
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}}}}}';
|
|
125
|
+
for (const item of pullRequests) {
|
|
126
|
+
const closingIssues = [];
|
|
127
|
+
let cursor = null;
|
|
128
|
+
for (;;) {
|
|
129
|
+
const args = ['api', 'graphql', '-f', `query=${closingIssuesQuery}`, '-F', `owner=${owner}`, '-F', `name=${name}`, '-F', `number=${String(item.number)}`];
|
|
130
|
+
if (cursor)
|
|
131
|
+
args.push('-F', `cursor=${cursor}`);
|
|
132
|
+
const response = client.json(args, { cwd: options.cwd });
|
|
133
|
+
if (!response.ok)
|
|
134
|
+
return failure(response.error);
|
|
135
|
+
const connection = response.value.data?.repository?.pullRequest?.closingIssuesReferences;
|
|
136
|
+
if (!connection || !Array.isArray(connection.nodes) || !connection.pageInfo || typeof connection.pageInfo.hasNextPage !== 'boolean') {
|
|
137
|
+
return failure({ code: 'INVALID_PLATFORM_RESPONSE', message: `Closing Issue data is incomplete for pull request ${String(item.number)}`, retryable: false });
|
|
138
|
+
}
|
|
139
|
+
closingIssues.push(...connection.nodes.map((issue) => ({
|
|
140
|
+
number: Number(issue.number), title: String(issue.title || ''), url: String(issue.url || ''),
|
|
141
|
+
author: normalizePlatformAuthor(issue.author)
|
|
142
|
+
})));
|
|
143
|
+
if (!connection.pageInfo.hasNextPage)
|
|
144
|
+
break;
|
|
145
|
+
if (!connection.pageInfo.endCursor || connection.pageInfo.endCursor === cursor) {
|
|
146
|
+
return failure({ code: 'PAGINATION_INVALID', message: `Closing Issue pagination is incomplete for pull request ${String(item.number)}`, retryable: false });
|
|
147
|
+
}
|
|
148
|
+
cursor = connection.pageInfo.endCursor;
|
|
149
|
+
}
|
|
150
|
+
resolvedPullRequests.push({
|
|
151
|
+
...item,
|
|
152
|
+
author: normalizePlatformAuthor(item.author || null),
|
|
153
|
+
closingIssues
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const commits = input.commitOids.map((sha) => ({
|
|
157
|
+
sha,
|
|
158
|
+
url: `https://github.com/${input.repository}/commit/${sha}`,
|
|
159
|
+
pullRequestNumbers: pullRequestNumbersByCommit.get(sha) || [],
|
|
160
|
+
authors: authors.get(sha) || []
|
|
161
|
+
}));
|
|
162
|
+
return { status: 'no-op', changed: false, history, pullRequests: resolvedPullRequests, commits, error: null };
|
|
94
163
|
}
|
|
95
164
|
export { fetchGitHubReleaseNoteData, normalizeGitHubActor, publishGitHubReleaseNotes };
|
|
96
165
|
//# sourceMappingURL=github-release-notes.js.map
|
|
@@ -242,22 +242,45 @@ type RepositoryMetadataSnapshot = {
|
|
|
242
242
|
}>;
|
|
243
243
|
}>;
|
|
244
244
|
};
|
|
245
|
+
type ReleaseNoteAuthor = {
|
|
246
|
+
name: string;
|
|
247
|
+
login: string | null;
|
|
248
|
+
bot: boolean;
|
|
249
|
+
resolution: 'platform-user' | 'platform-noreply' | 'unresolved';
|
|
250
|
+
};
|
|
251
|
+
type ReleaseNoteIssue = {
|
|
252
|
+
id: string;
|
|
253
|
+
identity: ResourceIdentity;
|
|
254
|
+
number: number;
|
|
255
|
+
title: string;
|
|
256
|
+
displayUrl: string;
|
|
257
|
+
author: ReleaseNoteAuthor | null;
|
|
258
|
+
};
|
|
259
|
+
type ReleaseNotePullRequest = {
|
|
260
|
+
id: string;
|
|
261
|
+
identity: ResourceIdentity;
|
|
262
|
+
number: number;
|
|
263
|
+
title: string;
|
|
264
|
+
body: string;
|
|
265
|
+
mergedAt: string;
|
|
266
|
+
displayUrl: string;
|
|
267
|
+
labels: string[];
|
|
268
|
+
author: ReleaseNoteAuthor | null;
|
|
269
|
+
closingIssues: ReleaseNoteIssue[];
|
|
270
|
+
};
|
|
245
271
|
type ReleaseNotesFacts = {
|
|
246
272
|
history: Array<{
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
author: {
|
|
251
|
-
id?: string;
|
|
252
|
-
name?: string;
|
|
253
|
-
} | null;
|
|
273
|
+
tag: string;
|
|
274
|
+
body: string;
|
|
275
|
+
url: string | null;
|
|
254
276
|
}>;
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
277
|
+
commits: Array<{
|
|
278
|
+
sha: string;
|
|
279
|
+
url: string | null;
|
|
280
|
+
pullRequestNumbers: number[];
|
|
281
|
+
authors: ReleaseNoteAuthor[];
|
|
260
282
|
}>;
|
|
283
|
+
mergedPullRequests: ReleaseNotePullRequest[];
|
|
261
284
|
};
|
|
262
285
|
type MilestoneReconciliation = {
|
|
263
286
|
changed: boolean;
|
|
@@ -276,13 +299,13 @@ type VerificationRemoteFacts = {
|
|
|
276
299
|
};
|
|
277
300
|
type PlatformProviderFactoryInput = {
|
|
278
301
|
providerType: string;
|
|
279
|
-
contractVersion:
|
|
302
|
+
contractVersion: 2;
|
|
280
303
|
repositoryRoot: string;
|
|
281
304
|
config: Readonly<Record<string, JsonValue>>;
|
|
282
305
|
};
|
|
283
306
|
type PlatformProvider = {
|
|
284
307
|
type: string;
|
|
285
|
-
contractVersion:
|
|
308
|
+
contractVersion: 2;
|
|
286
309
|
identity?: ProviderIdentityDeclaration;
|
|
287
310
|
context: {
|
|
288
311
|
resolve(input: {
|
|
@@ -548,8 +571,8 @@ type PlatformProvider = {
|
|
|
548
571
|
};
|
|
549
572
|
};
|
|
550
573
|
type PlatformProviderFactory = (input: PlatformProviderFactoryInput) => Promise<PlatformProvider>;
|
|
551
|
-
declare const PLATFORM_PROVIDER_CONTRACT_VERSION:
|
|
574
|
+
declare const PLATFORM_PROVIDER_CONTRACT_VERSION: 2;
|
|
552
575
|
declare function isPlatformProviderFactory(value: unknown): value is PlatformProviderFactory;
|
|
553
576
|
declare function validatePlatformProvider(value: unknown, providerType: string): ProviderResult<PlatformProvider>;
|
|
554
577
|
export { PLATFORM_PROVIDER_CONTRACT_VERSION, isPlatformProviderFactory, validatePlatformProvider };
|
|
555
|
-
export type { ChangeRequestSnapshot, CheckLogSnapshot, CheckRunSnapshot, GitEvidenceSnapshot, IssueSnapshot, JsonValue, MilestoneReconciliation, MilestoneDefinition, MilestoneInitialization, LabelDefinition, LabelReconciliation, MutationIdentity, MutationReceipt, PlatformCapabilities, PlatformContextSnapshot, PlatformError, PlatformProvider, PlatformProviderFactory, PlatformProviderFactoryInput, ProviderOperationContext, ProviderResult, ReleaseSnapshot, RemoteCommentSnapshot, RequiredCheckSnapshot, RepositoryMetadataSnapshot, ReleaseNotesFacts, ResourceIdentity, SecurityAlertKind, SecurityAlertSnapshot, ReviewSnapshot, VerificationRemoteFacts };
|
|
578
|
+
export type { ChangeRequestSnapshot, CheckLogSnapshot, CheckRunSnapshot, GitEvidenceSnapshot, IssueSnapshot, JsonValue, MilestoneReconciliation, MilestoneDefinition, MilestoneInitialization, LabelDefinition, LabelReconciliation, MutationIdentity, MutationReceipt, PlatformCapabilities, PlatformContextSnapshot, PlatformError, PlatformProvider, PlatformProviderFactory, PlatformProviderFactoryInput, ProviderOperationContext, ProviderResult, ReleaseSnapshot, RemoteCommentSnapshot, RequiredCheckSnapshot, RepositoryMetadataSnapshot, ReleaseNotesFacts, ReleaseNoteAuthor, ReleaseNoteIssue, ReleaseNotePullRequest, ResourceIdentity, SecurityAlertKind, SecurityAlertSnapshot, ReviewSnapshot, VerificationRemoteFacts };
|
|
@@ -78,6 +78,14 @@ function arrayValue(value, label) {
|
|
|
78
78
|
throw new Error(`${label} must be an array`);
|
|
79
79
|
return value;
|
|
80
80
|
}
|
|
81
|
+
function positiveNumberValue(value, label) {
|
|
82
|
+
if (!Number.isSafeInteger(value) || Number(value) <= 0)
|
|
83
|
+
throw new Error(`${label} must be a positive integer`);
|
|
84
|
+
return Number(value);
|
|
85
|
+
}
|
|
86
|
+
function stringArray(value, label) {
|
|
87
|
+
return arrayValue(value, label).map((entry, index) => stringValue(entry, `${label}[${index}]`));
|
|
88
|
+
}
|
|
81
89
|
function stableUnique(values, key, label) {
|
|
82
90
|
const seen = new Set();
|
|
83
91
|
for (const value of values) {
|
|
@@ -88,6 +96,16 @@ function stableUnique(values, key, label) {
|
|
|
88
96
|
}
|
|
89
97
|
return [...values].sort((left, right) => key(left).localeCompare(key(right)));
|
|
90
98
|
}
|
|
99
|
+
function uniqueInOrder(values, key, label) {
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
for (const value of values) {
|
|
102
|
+
const identityKey = key(value);
|
|
103
|
+
if (seen.has(identityKey))
|
|
104
|
+
throw new Error(`${label} contains duplicate identities`);
|
|
105
|
+
seen.add(identityKey);
|
|
106
|
+
}
|
|
107
|
+
return values;
|
|
108
|
+
}
|
|
91
109
|
function declaredIdentityKind(declaration, resourceKind) {
|
|
92
110
|
const kind = declaration?.[resourceKind];
|
|
93
111
|
return kind === 'id' || kind === 'number' || kind === 'key' ? kind : undefined;
|
|
@@ -358,21 +376,73 @@ function validateMetadata(value) {
|
|
|
358
376
|
}
|
|
359
377
|
function validateReleaseNotes(value, declaration) {
|
|
360
378
|
const item = record(value, 'release notes');
|
|
361
|
-
exactKeys(item, ['history', '
|
|
379
|
+
exactKeys(item, ['history', 'commits', 'mergedPullRequests'], 'release notes');
|
|
380
|
+
const releaseAuthor = (raw, label) => {
|
|
381
|
+
if (raw === null)
|
|
382
|
+
return null;
|
|
383
|
+
const entry = record(raw, label);
|
|
384
|
+
exactKeys(entry, ['name', 'login', 'bot', 'resolution'], label);
|
|
385
|
+
if (typeof entry.bot !== 'boolean')
|
|
386
|
+
throw new Error(`${label}.bot must be boolean`);
|
|
387
|
+
if (!['platform-user', 'platform-noreply', 'unresolved'].includes(String(entry.resolution))) {
|
|
388
|
+
throw new Error(`${label}.resolution is invalid`);
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
name: stringValue(entry.name, `${label}.name`, true),
|
|
392
|
+
login: nullableString(entry.login, `${label}.login`),
|
|
393
|
+
bot: entry.bot,
|
|
394
|
+
resolution: entry.resolution
|
|
395
|
+
};
|
|
396
|
+
};
|
|
397
|
+
const issue = (raw, label) => {
|
|
398
|
+
const entry = record(raw, label);
|
|
399
|
+
exactKeys(entry, ['id', 'identity', 'number', 'title', 'displayUrl', 'author'], label);
|
|
400
|
+
return {
|
|
401
|
+
id: stringValue(entry.id, `${label}.id`),
|
|
402
|
+
identity: identity(entry.identity, `${label}.identity`, declaredIdentityKind(declaration, 'issue')),
|
|
403
|
+
number: positiveNumberValue(entry.number, `${label}.number`),
|
|
404
|
+
title: stringValue(entry.title, `${label}.title`, true),
|
|
405
|
+
displayUrl: stringValue(entry.displayUrl, `${label}.displayUrl`, true),
|
|
406
|
+
author: releaseAuthor(entry.author, `${label}.author`)
|
|
407
|
+
};
|
|
408
|
+
};
|
|
362
409
|
return {
|
|
363
|
-
history:
|
|
410
|
+
history: uniqueInOrder(arrayValue(item.history, 'release notes.history').map((raw) => {
|
|
364
411
|
const entry = record(raw, 'release notes.history[]');
|
|
365
|
-
exactKeys(entry, ['
|
|
366
|
-
return {
|
|
367
|
-
}), (entry) => entry.
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
412
|
+
exactKeys(entry, ['tag', 'body', 'url'], 'release notes.history[]');
|
|
413
|
+
return { tag: stringValue(entry.tag, 'history.tag'), body: stringValue(entry.body, 'history.body', true), url: nullableString(entry.url, 'history.url') };
|
|
414
|
+
}), (entry) => entry.tag, 'release notes.history'),
|
|
415
|
+
commits: stableUnique(arrayValue(item.commits, 'release notes.commits').map((raw) => {
|
|
416
|
+
const entry = record(raw, 'release notes.commits[]');
|
|
417
|
+
exactKeys(entry, ['sha', 'url', 'pullRequestNumbers', 'authors'], 'release notes.commits[]');
|
|
418
|
+
return {
|
|
419
|
+
sha: stringValue(entry.sha, 'commits.sha'),
|
|
420
|
+
url: nullableString(entry.url, 'commits.url'),
|
|
421
|
+
pullRequestNumbers: stableUnique(arrayValue(entry.pullRequestNumbers, 'commits.pullRequestNumbers').map((number) => positiveNumberValue(number, 'commits.pullRequestNumbers[]')), (number) => String(number), 'commits.pullRequestNumbers'),
|
|
422
|
+
authors: arrayValue(entry.authors, 'commits.authors').map((rawAuthor) => {
|
|
423
|
+
const normalized = releaseAuthor(rawAuthor, 'commits.author');
|
|
424
|
+
if (!normalized)
|
|
425
|
+
throw new Error('commits.author must identify an author');
|
|
426
|
+
return normalized;
|
|
427
|
+
})
|
|
428
|
+
};
|
|
429
|
+
}), (entry) => entry.sha, 'release notes.commits'),
|
|
430
|
+
mergedPullRequests: stableUnique(arrayValue(item.mergedPullRequests, 'release notes.mergedPullRequests').map((raw) => {
|
|
431
|
+
const entry = record(raw, 'release notes.mergedPullRequests[]');
|
|
432
|
+
exactKeys(entry, ['id', 'identity', 'number', 'title', 'body', 'mergedAt', 'displayUrl', 'labels', 'author', 'closingIssues'], 'release notes.mergedPullRequests[]');
|
|
433
|
+
return {
|
|
434
|
+
id: stringValue(entry.id, 'pullRequest.id'),
|
|
435
|
+
identity: identity(entry.identity, 'pullRequest.identity', declaredIdentityKind(declaration, 'pull-request')),
|
|
436
|
+
number: positiveNumberValue(entry.number, 'pullRequest.number'),
|
|
437
|
+
title: stringValue(entry.title, 'pullRequest.title', true),
|
|
438
|
+
body: stringValue(entry.body, 'pullRequest.body', true),
|
|
439
|
+
mergedAt: utcTimestamp(entry.mergedAt, 'pullRequest.mergedAt'),
|
|
440
|
+
displayUrl: stringValue(entry.displayUrl, 'pullRequest.displayUrl', true),
|
|
441
|
+
labels: stringArray(entry.labels, 'pullRequest.labels'),
|
|
442
|
+
author: releaseAuthor(entry.author, 'pullRequest.author'),
|
|
443
|
+
closingIssues: stableUnique(arrayValue(entry.closingIssues, 'pullRequest.closingIssues').map((entry) => issue(entry, 'pullRequest.closingIssues[]')), (entry) => serializeResourceIdentity(entry.identity), 'pullRequest.closingIssues')
|
|
444
|
+
};
|
|
445
|
+
}), (entry) => serializeResourceIdentity(entry.identity), 'release notes.mergedPullRequests')
|
|
376
446
|
};
|
|
377
447
|
}
|
|
378
448
|
function utcTimestamp(value, label) {
|
|
@@ -154,16 +154,16 @@ async function releaseNoteContext(input, options = {}) {
|
|
|
154
154
|
return { ...baseResult(platform.error.retryable ? 'blocked' : 'failed'), error: providerError(platform.error, 'PLATFORM_PROVIDER_OPERATION_FAILED') };
|
|
155
155
|
const commits = commitFacts.map((commit) => {
|
|
156
156
|
const seen = new Set();
|
|
157
|
-
const
|
|
158
|
-
|
|
157
|
+
const facts = platform.value.commits.find((item) => item.sha === commit.oid);
|
|
158
|
+
const authors = (facts?.authors || [])
|
|
159
159
|
.filter((actor) => {
|
|
160
|
-
const key = actor.
|
|
160
|
+
const key = actor.login || actor.name;
|
|
161
161
|
if (seen.has(key))
|
|
162
162
|
return false;
|
|
163
163
|
seen.add(key);
|
|
164
164
|
return true;
|
|
165
165
|
});
|
|
166
|
-
return { ...commit, authors };
|
|
166
|
+
return { ...commit, url: facts?.url || null, pullRequestNumbers: facts?.pullRequestNumbers || [], authors };
|
|
167
167
|
});
|
|
168
168
|
const pullRequests = platform.value.mergedPullRequests
|
|
169
169
|
.map((item) => ({
|
|
@@ -174,14 +174,13 @@ async function releaseNoteContext(input, options = {}) {
|
|
|
174
174
|
url: item.displayUrl || '',
|
|
175
175
|
mergedAt: item.mergedAt || '',
|
|
176
176
|
labels: item.labels || [],
|
|
177
|
-
author: item.author
|
|
178
|
-
closingIssues:
|
|
177
|
+
author: item.author,
|
|
178
|
+
closingIssues: item.closingIssues.map((issue) => ({
|
|
179
179
|
number: issue.number || resourceIdentityNumber(issue.identity) || 0,
|
|
180
180
|
...(issue.identity ? { identity: issue.identity } : {}),
|
|
181
181
|
title: issue.title,
|
|
182
182
|
url: issue.displayUrl || '',
|
|
183
|
-
|
|
184
|
-
author: issue.author ? { login: issue.author.name || issue.author.id || '', bot: false } : null
|
|
183
|
+
author: issue.author
|
|
185
184
|
}))
|
|
186
185
|
}))
|
|
187
186
|
.sort((left, right) => left.number - right.number);
|
|
@@ -154,7 +154,7 @@ export async function runInSandbox(request, options = {}) {
|
|
|
154
154
|
allowRecreate: request.recreate,
|
|
155
155
|
recreate: async () => {
|
|
156
156
|
const { create } = await import('./commands/create.js');
|
|
157
|
-
await create([request.taskRef, '--no-refresh']);
|
|
157
|
+
await create([request.taskRef, '--no-refresh'], { runProjectInitCommand: false });
|
|
158
158
|
}
|
|
159
159
|
})).container;
|
|
160
160
|
}
|