@fitlab-ai/agent-infra 0.9.0 → 0.9.1
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 +6 -1
- package/README.zh-CN.md +5 -1
- package/dist/bin/internal-cli.js +5 -0
- package/dist/lib/internal/platform-release-notes.js +53 -0
- package/dist/lib/internal/release-workflow.js +105 -2
- package/dist/lib/platform/github-release-notes.js +96 -0
- package/dist/lib/platform/issues.js +4 -2
- package/dist/lib/platform/merged-pr-equivalence.js +54 -0
- package/dist/lib/platform/pr-checks.js +6 -6
- package/dist/lib/platform/pull-requests.js +3 -1
- package/dist/lib/platform/release-notes.js +143 -0
- package/dist/lib/platform/verification-required.js +29 -5
- package/dist/lib/sandbox/build-proxy.js +63 -0
- package/dist/lib/sandbox/commands/create.js +57 -9
- package/dist/lib/sandbox/commands/rebuild.js +24 -7
- package/dist/lib/sandbox/image-build.js +2 -1
- package/dist/lib/sandbox/runtimes/ai-tools.dockerfile +24 -3
- package/dist/lib/sandbox/runtimes/base.dockerfile +8 -1
- package/dist/lib/sandbox/runtimes/java17.dockerfile +8 -1
- package/dist/lib/sandbox/runtimes/java21.dockerfile +8 -1
- package/dist/lib/sandbox/runtimes/node20.dockerfile +8 -1
- package/dist/lib/sandbox/runtimes/node22.dockerfile +8 -1
- package/dist/lib/sandbox/runtimes/python3.dockerfile +8 -1
- package/dist/lib/sandbox/shell.js +2 -1
- package/dist/lib/task/commands/issue-body.js +10 -5
- package/dist/lib/task/events.js +1 -1
- package/dist/lib/task/issue-form.js +2 -0
- package/dist/lib/task/review-fingerprint.js +9 -0
- package/dist/lib/task/verification-engine.js +34 -17
- package/dist/lib/task/verification-review.js +1 -1
- package/dist/lib/task/verification.js +1 -1
- package/lib/internal/platform-release-notes.ts +55 -0
- package/lib/internal/release-workflow.ts +105 -3
- package/lib/platform/github-release-notes.ts +132 -0
- package/lib/platform/issues.ts +6 -4
- package/lib/platform/merged-pr-equivalence.ts +70 -0
- package/lib/platform/pr-checks.ts +8 -7
- package/lib/platform/pull-requests.ts +9 -3
- package/lib/platform/release-notes.ts +167 -0
- package/lib/platform/verification-required.ts +31 -5
- package/lib/sandbox/build-proxy.ts +94 -0
- package/lib/sandbox/commands/create.ts +77 -11
- package/lib/sandbox/commands/rebuild.ts +29 -7
- package/lib/sandbox/image-build.ts +4 -1
- package/lib/sandbox/runtimes/ai-tools.dockerfile +24 -3
- package/lib/sandbox/runtimes/base.dockerfile +8 -1
- package/lib/sandbox/runtimes/java17.dockerfile +8 -1
- package/lib/sandbox/runtimes/java21.dockerfile +8 -1
- package/lib/sandbox/runtimes/node20.dockerfile +8 -1
- package/lib/sandbox/runtimes/node22.dockerfile +8 -1
- package/lib/sandbox/runtimes/python3.dockerfile +8 -1
- package/lib/sandbox/shell.ts +3 -1
- package/lib/task/commands/issue-body.ts +10 -5
- package/lib/task/events.ts +1 -1
- package/lib/task/issue-form.ts +4 -0
- package/lib/task/review-fingerprint.ts +10 -1
- package/lib/task/verification-engine.ts +33 -17
- package/lib/task/verification-review.ts +1 -0
- package/lib/task/verification.ts +1 -1
- package/package.json +1 -1
- package/templates/.agents/rules/release-commands.en.md +2 -2
- package/templates/.agents/rules/release-commands.github.en.md +10 -40
- package/templates/.agents/rules/release-commands.github.zh-CN.md +9 -39
- package/templates/.agents/rules/release-commands.zh-CN.md +2 -2
- package/templates/.agents/rules/review-handshake.en.md +2 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +2 -1
- package/templates/.agents/rules/testing-discipline.en.md +20 -2
- package/templates/.agents/rules/testing-discipline.zh-CN.md +20 -2
- package/templates/.agents/skills/analyze-task/SKILL.en.md +4 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +4 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/complete-task/config/verify.en.json +0 -1
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +0 -1
- package/templates/.agents/skills/create-release-note/SKILL.en.md +17 -38
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +16 -37
- package/templates/.agents/skills/create-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/create-task/reference/context-capture.en.md +29 -0
- package/templates/.agents/skills/create-task/reference/context-capture.zh-CN.md +29 -0
- package/templates/.agents/templates/task.en.md +18 -0
- package/templates/.agents/templates/task.zh-CN.md +18 -0
package/README.md
CHANGED
|
@@ -35,9 +35,14 @@ agent-infra standardizes that shared infrastructure. It gives every supported AI
|
|
|
35
35
|
## See it in Action
|
|
36
36
|
|
|
37
37
|
<p align="center">
|
|
38
|
-
<img src="./assets/demo-init.gif" alt="CLI install and initialize demo" width="
|
|
38
|
+
<img src="./assets/demo-init.gif" alt="CLI install and initialize demo" width="720">
|
|
39
39
|
</p>
|
|
40
40
|
|
|
41
|
+
> The demo is stored with Git LFS. Install Git LFS and run `git lfs install`
|
|
42
|
+
> before cloning, or run `git lfs pull` in an existing checkout. If the image
|
|
43
|
+
> is a small text pointer or is missing, verify `git lfs ls-files`, then retry
|
|
44
|
+
> `git lfs pull`.
|
|
45
|
+
|
|
41
46
|
Once initialized, open the project in your AI TUI and install the latest skills:
|
|
42
47
|
|
|
43
48
|
```bash
|
package/README.zh-CN.md
CHANGED
|
@@ -35,9 +35,13 @@ agent-infra 的目标就是把这层共享基础设施标准化。它为所有
|
|
|
35
35
|
## 实战演示
|
|
36
36
|
|
|
37
37
|
<p align="center">
|
|
38
|
-
<img src="./assets/demo-init.gif" alt="CLI 安装初始化演示" width="
|
|
38
|
+
<img src="./assets/demo-init.gif" alt="CLI 安装初始化演示" width="720">
|
|
39
39
|
</p>
|
|
40
40
|
|
|
41
|
+
> 演示动画通过 Git LFS 存储。克隆前请安装 Git LFS 并运行 `git lfs install`;
|
|
42
|
+
> 已有检出可运行 `git lfs pull`。如果图片变成很小的文本指针或缺失,请先用
|
|
43
|
+
> `git lfs ls-files` 核对跟踪状态,再重试 `git lfs pull`。
|
|
44
|
+
|
|
41
45
|
完成初始化后,在你的 AI TUI 中打开项目并安装最新 skills:
|
|
42
46
|
|
|
43
47
|
```bash
|
package/dist/bin/internal-cli.js
CHANGED
|
@@ -16,6 +16,11 @@ switch (command) {
|
|
|
16
16
|
await releaseWorkflow(process.argv.slice(3));
|
|
17
17
|
break;
|
|
18
18
|
}
|
|
19
|
+
case 'platform-release-notes': {
|
|
20
|
+
const { platformReleaseNotes } = await import('../lib/internal/platform-release-notes.js');
|
|
21
|
+
platformReleaseNotes(process.argv.slice(3));
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
19
24
|
case 'platform-context': {
|
|
20
25
|
const { platformContext } = await import('../lib/internal/platform-context.js');
|
|
21
26
|
platformContext(process.argv.slice(3));
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { publishReleaseNotes, releaseNoteContext } from '../platform/release-notes.js';
|
|
4
|
+
function option(args, name) {
|
|
5
|
+
const index = args.indexOf(name);
|
|
6
|
+
return index >= 0 && args[index + 1] ? args[index + 1] : null;
|
|
7
|
+
}
|
|
8
|
+
function finish(result) {
|
|
9
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
10
|
+
process.exitCode = result.status === 'blocked' ? 2 : result.status === 'failed' ? 1 : 0;
|
|
11
|
+
}
|
|
12
|
+
function platformReleaseNotes(args = []) {
|
|
13
|
+
const [action, ...rest] = args;
|
|
14
|
+
const cwd = path.resolve(option(rest, '--cwd') || process.cwd());
|
|
15
|
+
if (action === 'context') {
|
|
16
|
+
const historyRaw = option(rest, '--history-limit');
|
|
17
|
+
finish(releaseNoteContext({
|
|
18
|
+
fromTag: option(rest, '--from-tag') || '',
|
|
19
|
+
toTag: option(rest, '--to-tag') || '',
|
|
20
|
+
branch: option(rest, '--branch') || '',
|
|
21
|
+
historyLimit: historyRaw === null ? undefined : Number(historyRaw)
|
|
22
|
+
}, { cwd }));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (action === 'publish') {
|
|
26
|
+
const notesPath = option(rest, '--notes-file');
|
|
27
|
+
if (!notesPath) {
|
|
28
|
+
finish({ status: 'failed', changed: false, error: { code: 'RELEASE_NOTES_INPUT_INVALID', message: 'notes-file is required', retryable: false } });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
let resolvedNotesPath = notesPath;
|
|
32
|
+
if (notesPath === '-') {
|
|
33
|
+
resolvedNotesPath = path.join(process.env.TMPDIR || '/tmp', `agent-infra-release-notes-${process.pid}.md`);
|
|
34
|
+
fs.writeFileSync(resolvedNotesPath, fs.readFileSync(0));
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
finish(publishReleaseNotes({
|
|
38
|
+
tag: option(rest, '--tag') || '',
|
|
39
|
+
title: option(rest, '--title') || option(rest, '--tag') || '',
|
|
40
|
+
notesFile: resolvedNotesPath,
|
|
41
|
+
dryRun: rest.includes('--dry-run')
|
|
42
|
+
}, { cwd }));
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
if (notesPath === '-')
|
|
46
|
+
fs.rmSync(resolvedNotesPath, { force: true });
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
finish({ status: 'failed', changed: false, error: { code: 'RELEASE_NOTES_INPUT_INVALID', message: 'Usage: platform-release-notes context|publish', retryable: false } });
|
|
51
|
+
}
|
|
52
|
+
export { platformReleaseNotes };
|
|
53
|
+
//# sourceMappingURL=platform-release-notes.js.map
|
|
@@ -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 { spawnSync } from 'node:child_process';
|
|
4
5
|
import semver from 'semver';
|
|
5
6
|
import { commitExplicitPaths, inspectGitWorkflow, pushGitRefs } from '../git/workflow.js';
|
|
@@ -9,7 +10,97 @@ import { releaseSnapshot } from '../release/workflow.js';
|
|
|
9
10
|
function command(cwd, executable, args) {
|
|
10
11
|
return spawnSync(executable, args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
11
12
|
}
|
|
13
|
+
const DEMO_INPUT_PATHS = [
|
|
14
|
+
'assets/demo-init.tape',
|
|
15
|
+
'scripts/demo-regen.sh',
|
|
16
|
+
'scripts/normalize-gif-duration.py',
|
|
17
|
+
'bin/cli.ts',
|
|
18
|
+
'lib/init.ts',
|
|
19
|
+
'lib/log.ts',
|
|
20
|
+
'lib/prompt.ts',
|
|
21
|
+
'lib/paths.ts',
|
|
22
|
+
'lib/render.ts',
|
|
23
|
+
'lib/builtin-tuis.ts',
|
|
24
|
+
'lib/sandbox/engines/',
|
|
25
|
+
'src/sync-templates.js',
|
|
26
|
+
'templates/'
|
|
27
|
+
];
|
|
28
|
+
const DEMO_DIGEST_PATH = 'assets/demo-init.inputs.sha256';
|
|
29
|
+
const DEMO_OUTPUT_PATH = 'assets/demo-init.gif';
|
|
30
|
+
const DEMO_MAX_BYTES = 4 * 1024 * 1024;
|
|
31
|
+
function demoInputFiles(cwd) {
|
|
32
|
+
const files = new Set();
|
|
33
|
+
for (const input of DEMO_INPUT_PATHS) {
|
|
34
|
+
if (input.endsWith('/')) {
|
|
35
|
+
const listed = command(cwd, 'git', ['ls-files', '--', input]);
|
|
36
|
+
if (listed.status !== 0)
|
|
37
|
+
throw new Error(String(listed.stderr || `Unable to list ${input}`));
|
|
38
|
+
const directoryFiles = String(listed.stdout).split('\n').filter(Boolean);
|
|
39
|
+
if (!directoryFiles.length)
|
|
40
|
+
throw new Error(`Canonical demo input directory is empty: ${input}`);
|
|
41
|
+
for (const file of directoryFiles)
|
|
42
|
+
files.add(file.replaceAll('\\', '/'));
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
files.add(input);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const sorted = [...files].sort((left, right) => Buffer.from(left).compare(Buffer.from(right)));
|
|
49
|
+
if (!sorted.length)
|
|
50
|
+
throw new Error('Canonical demo input set is empty');
|
|
51
|
+
for (const file of sorted) {
|
|
52
|
+
const absolute = path.join(cwd, file);
|
|
53
|
+
if (!fs.statSync(absolute).isFile())
|
|
54
|
+
throw new Error(`Canonical demo input is not a file: ${file}`);
|
|
55
|
+
}
|
|
56
|
+
return sorted;
|
|
57
|
+
}
|
|
58
|
+
function computeDemoInputDigest(cwd) {
|
|
59
|
+
const hash = crypto.createHash('sha256');
|
|
60
|
+
for (const file of demoInputFiles(cwd)) {
|
|
61
|
+
const bytes = fs.readFileSync(path.join(cwd, file));
|
|
62
|
+
hash.update(file);
|
|
63
|
+
hash.update('\0');
|
|
64
|
+
hash.update(String(bytes.byteLength));
|
|
65
|
+
hash.update('\0');
|
|
66
|
+
hash.update(bytes);
|
|
67
|
+
hash.update('\0');
|
|
68
|
+
}
|
|
69
|
+
return hash.digest('hex');
|
|
70
|
+
}
|
|
71
|
+
function validGif(filePath) {
|
|
72
|
+
if (!fs.existsSync(filePath))
|
|
73
|
+
return false;
|
|
74
|
+
const header = fs.readFileSync(filePath).subarray(0, 6).toString('ascii');
|
|
75
|
+
return header === 'GIF87a' || header === 'GIF89a';
|
|
76
|
+
}
|
|
77
|
+
function writeDigestAtomically(cwd, digest) {
|
|
78
|
+
const target = path.join(cwd, DEMO_DIGEST_PATH);
|
|
79
|
+
const temporary = `${target}.tmp-${process.pid}`;
|
|
80
|
+
fs.writeFileSync(temporary, `${digest}\n`);
|
|
81
|
+
fs.renameSync(temporary, target);
|
|
82
|
+
}
|
|
12
83
|
function runOptionalDemo(cwd, run = command) {
|
|
84
|
+
let digest;
|
|
85
|
+
try {
|
|
86
|
+
digest = computeDemoInputDigest(cwd);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
return { status: 'failed', reasonCode: 'DEMO_DIGEST_FAILED', message: String(error), outputPath: null };
|
|
90
|
+
}
|
|
91
|
+
const digestPath = path.join(cwd, DEMO_DIGEST_PATH);
|
|
92
|
+
const priorDigest = fs.existsSync(digestPath) ? fs.readFileSync(digestPath, 'utf8').trim() : '';
|
|
93
|
+
if (/^[0-9a-f]{64}$/.test(priorDigest) && priorDigest === digest) {
|
|
94
|
+
return { status: 'skipped', reasonCode: 'DEMO_INPUTS_UNCHANGED', message: null, outputPath: null };
|
|
95
|
+
}
|
|
96
|
+
const lfs = run(cwd, 'git', ['lfs', 'version']);
|
|
97
|
+
if (lfs.status !== 0) {
|
|
98
|
+
return { status: 'failed', reasonCode: 'GIT_LFS_MISSING', message: String(lfs.stderr || lfs.stdout), outputPath: null };
|
|
99
|
+
}
|
|
100
|
+
const lfsAttribute = run(cwd, 'git', ['check-attr', 'filter', '--', DEMO_OUTPUT_PATH]);
|
|
101
|
+
if (lfsAttribute.status !== 0 || !String(lfsAttribute.stdout).trim().endsWith(': lfs')) {
|
|
102
|
+
return { status: 'failed', reasonCode: 'GIT_LFS_MISSING', message: `${DEMO_OUTPUT_PATH} is not tracked by Git LFS`, outputPath: null };
|
|
103
|
+
}
|
|
13
104
|
const vhs = run(cwd, 'vhs', ['--version']);
|
|
14
105
|
if (vhs.status !== 0)
|
|
15
106
|
return { status: 'skipped', reasonCode: 'VHS_MISSING', message: null, outputPath: null };
|
|
@@ -25,7 +116,7 @@ function runOptionalDemo(cwd, run = command) {
|
|
|
25
116
|
outputPath: null
|
|
26
117
|
};
|
|
27
118
|
}
|
|
28
|
-
const outputPath =
|
|
119
|
+
const outputPath = DEMO_OUTPUT_PATH;
|
|
29
120
|
if (!fs.existsSync(path.join(cwd, outputPath))) {
|
|
30
121
|
return {
|
|
31
122
|
status: 'failed',
|
|
@@ -34,6 +125,18 @@ function runOptionalDemo(cwd, run = command) {
|
|
|
34
125
|
outputPath: null
|
|
35
126
|
};
|
|
36
127
|
}
|
|
128
|
+
const absoluteOutput = path.join(cwd, outputPath);
|
|
129
|
+
if (!validGif(absoluteOutput)) {
|
|
130
|
+
return { status: 'failed', reasonCode: 'DEMO_OUTPUT_INVALID', message: `${outputPath} is not a GIF`, outputPath: null };
|
|
131
|
+
}
|
|
132
|
+
if (fs.statSync(absoluteOutput).size > DEMO_MAX_BYTES) {
|
|
133
|
+
return { status: 'failed', reasonCode: 'DEMO_OUTPUT_TOO_LARGE', message: `${outputPath} exceeds 4 MiB`, outputPath: null };
|
|
134
|
+
}
|
|
135
|
+
const pointer = run(cwd, 'git', ['lfs', 'pointer', `--file=${outputPath}`]);
|
|
136
|
+
if (pointer.status !== 0 || !String(pointer.stdout).includes(`size ${fs.statSync(absoluteOutput).size}`)) {
|
|
137
|
+
return { status: 'failed', reasonCode: 'DEMO_OUTPUT_INVALID', message: 'Git LFS could not produce a matching pointer', outputPath: null };
|
|
138
|
+
}
|
|
139
|
+
writeDigestAtomically(cwd, digest);
|
|
37
140
|
return { status: 'recorded', reasonCode: null, message: null, outputPath };
|
|
38
141
|
}
|
|
39
142
|
function git(cwd, args) {
|
|
@@ -213,5 +316,5 @@ async function releaseWorkflow(args = []) {
|
|
|
213
316
|
process.stdout.write(`${JSON.stringify({ ...pushed, demo, snapshot: releaseSnapshot(version, await inspectFacts(cwd, version)) })}\n`);
|
|
214
317
|
process.exitCode = pushed.status === 'failed' ? 1 : pushed.status === 'degraded' ? 2 : 0;
|
|
215
318
|
}
|
|
216
|
-
export { inspectFacts, releaseWorkflow, runOptionalDemo };
|
|
319
|
+
export { computeDemoInputDigest, inspectFacts, releaseWorkflow, runOptionalDemo };
|
|
217
320
|
//# sourceMappingURL=release-workflow.js.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { createGitHubClient } from './github-client.js';
|
|
2
|
+
function normalizeGitHubActor(actor) {
|
|
3
|
+
const name = String(actor.name || '').trim();
|
|
4
|
+
const email = actor.email ? String(actor.email).trim() : null;
|
|
5
|
+
const platformLogin = actor.user?.login?.trim().toLowerCase() || null;
|
|
6
|
+
const noReplyLogin = email
|
|
7
|
+
? /^(?:\d+\+)?([^@]+)@users\.noreply\.github\.com$/i.exec(email)?.[1]?.toLowerCase() || null
|
|
8
|
+
: null;
|
|
9
|
+
const login = platformLogin || noReplyLogin;
|
|
10
|
+
return {
|
|
11
|
+
name,
|
|
12
|
+
email,
|
|
13
|
+
login,
|
|
14
|
+
bot: Boolean(login?.endsWith('[bot]')),
|
|
15
|
+
resolution: platformLogin ? 'platform-user' : noReplyLogin ? 'platform-noreply' : 'unresolved'
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function failure(error) {
|
|
19
|
+
return {
|
|
20
|
+
status: error.retryable ? 'blocked' : 'failed',
|
|
21
|
+
changed: false,
|
|
22
|
+
operation: null,
|
|
23
|
+
url: null,
|
|
24
|
+
error
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function publishGitHubReleaseNotes(input, options = {}) {
|
|
28
|
+
const client = options.client ?? createGitHubClient();
|
|
29
|
+
const inspected = client.json(['release', 'view', input.tag, '--repo', input.repository, '--json', 'url'], { cwd: options.cwd });
|
|
30
|
+
if (!inspected.ok && inspected.error.code !== 'RESOURCE_NOT_FOUND')
|
|
31
|
+
return failure(inspected.error);
|
|
32
|
+
const exists = inspected.ok;
|
|
33
|
+
const operation = exists ? 'release:update-notes' : 'release:create';
|
|
34
|
+
if (input.dryRun) {
|
|
35
|
+
return {
|
|
36
|
+
status: 'planned',
|
|
37
|
+
changed: false,
|
|
38
|
+
operation,
|
|
39
|
+
url: inspected.ok ? inspected.value.url || null : null,
|
|
40
|
+
error: null
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const args = exists
|
|
44
|
+
? ['release', 'edit', input.tag, '--repo', input.repository, '--notes-file', input.notesFile]
|
|
45
|
+
: ['release', 'create', input.tag, '--repo', input.repository, '--title', input.title, '--notes-file', input.notesFile];
|
|
46
|
+
const written = client.text(args, { cwd: options.cwd, method: exists ? 'PATCH' : 'POST' });
|
|
47
|
+
if (!written.ok)
|
|
48
|
+
return failure(written.error);
|
|
49
|
+
return {
|
|
50
|
+
status: 'applied',
|
|
51
|
+
changed: true,
|
|
52
|
+
operation,
|
|
53
|
+
url: written.value || (inspected.ok ? inspected.value.url || null : null),
|
|
54
|
+
error: null
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function fetchGitHubReleaseNoteData(input, options = {}) {
|
|
58
|
+
const client = options.client ?? createGitHubClient();
|
|
59
|
+
const releases = client.json(['release', 'list', '--repo', input.repository, '--limit', String(input.historyLimit + 10), '--json', 'tagName,isDraft,isPrerelease'], { cwd: options.cwd });
|
|
60
|
+
if (!releases.ok)
|
|
61
|
+
return failure(releases.error);
|
|
62
|
+
const history = [];
|
|
63
|
+
for (const item of releases.value.filter((entry) => !entry.isDraft && !entry.isPrerelease).slice(0, input.historyLimit)) {
|
|
64
|
+
const viewed = client.json(['release', 'view', String(item.tagName), '--repo', input.repository, '--json', 'body,url'], { cwd: options.cwd });
|
|
65
|
+
if (!viewed.ok)
|
|
66
|
+
return failure(viewed.error);
|
|
67
|
+
history.push({ tag: String(item.tagName), body: String(viewed.value.body || ''), url: viewed.value.url || null });
|
|
68
|
+
}
|
|
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,closingIssuesReferences'], { cwd: options.cwd });
|
|
70
|
+
if (!prs.ok)
|
|
71
|
+
return failure(prs.error);
|
|
72
|
+
const authors = new Map();
|
|
73
|
+
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}}}}}}`;
|
|
75
|
+
const [owner, name] = input.repository.split('/');
|
|
76
|
+
const result = client.json([
|
|
77
|
+
'api', 'graphql', '-f', `query=${query}`, '-F', `owner=${owner}`, '-F', `name=${name}`, '-F', `oid=${oid}`
|
|
78
|
+
], { cwd: options.cwd });
|
|
79
|
+
if (!result.ok)
|
|
80
|
+
return failure(result.error);
|
|
81
|
+
const connection = result.value.data?.repository?.object?.authors;
|
|
82
|
+
if (connection?.pageInfo?.hasNextPage) {
|
|
83
|
+
return failure({ code: 'RELEASE_NOTES_AUTHORS_TRUNCATED', message: `Commit authors exceeded the supported page size for ${oid}`, retryable: false });
|
|
84
|
+
}
|
|
85
|
+
authors.set(oid, (connection?.nodes || []).map(normalizeGitHubActor));
|
|
86
|
+
}
|
|
87
|
+
const fromTime = Date.parse(input.fromTime);
|
|
88
|
+
const toTime = Date.parse(input.toTime);
|
|
89
|
+
const pullRequests = prs.value.filter((item) => {
|
|
90
|
+
const mergedAt = Date.parse(String(item.mergedAt || ''));
|
|
91
|
+
return Number.isFinite(mergedAt) && mergedAt > fromTime && mergedAt <= toTime;
|
|
92
|
+
});
|
|
93
|
+
return { status: 'no-op', changed: false, history, pullRequests, authors, error: null };
|
|
94
|
+
}
|
|
95
|
+
export { fetchGitHubReleaseNoteData, normalizeGitHubActor, publishGitHubReleaseNotes };
|
|
96
|
+
//# sourceMappingURL=github-release-notes.js.map
|
|
@@ -5,7 +5,7 @@ import { buildDefaultBody } from '../task/commands/issue-body.js';
|
|
|
5
5
|
import { extractTitle, parseTaskFrontmatter } from '../task/frontmatter.js';
|
|
6
6
|
import { requirementFieldLabels, renderTemplateBody } from '../task/issue-form.js';
|
|
7
7
|
import { resolveTaskRef } from '../task/resolve-ref.js';
|
|
8
|
-
import { extractSection } from '../task/sections.js';
|
|
8
|
+
import { extractSection, findSectionHeading } from '../task/sections.js';
|
|
9
9
|
import { writeTask } from '../task/write.js';
|
|
10
10
|
import { resolvePlatformContext } from './context.js';
|
|
11
11
|
import { createGitHubClient } from './github-client.js';
|
|
@@ -228,7 +228,9 @@ function deterministicIssueBody(repoRoot, content, taskType) {
|
|
|
228
228
|
return renderTemplateBody(fs.readFileSync(selected, 'utf8'), {
|
|
229
229
|
title: taskTitle(content),
|
|
230
230
|
description: extractSection(content, ['描述', 'Description']),
|
|
231
|
-
requirements: extractSection(content, ['需求', 'Requirements'])
|
|
231
|
+
requirements: extractSection(content, ['需求', 'Requirements']),
|
|
232
|
+
taskInput: extractSection(content, ['任务输入', 'Task Input']),
|
|
233
|
+
taskInputHeading: findSectionHeading(content, ['任务输入', 'Task Input'])
|
|
232
234
|
});
|
|
233
235
|
}
|
|
234
236
|
catch {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
const SHA_PATTERN = /^[0-9a-f]{40}$/i;
|
|
3
|
+
function git(cwd, args) {
|
|
4
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
|
|
5
|
+
}
|
|
6
|
+
function commitExists(gitRoot, sha) {
|
|
7
|
+
try {
|
|
8
|
+
git(gitRoot, ["cat-file", "-e", `${sha}^{commit}`]);
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function resolveReviewedHeadRelation(input) {
|
|
16
|
+
const { gitRoot, localHead, lastReviewedCommit, pullRequest, pathspecs } = input;
|
|
17
|
+
if (localHead === lastReviewedCommit && lastReviewedCommit === pullRequest.head.sha) {
|
|
18
|
+
return { status: "strict", reviewedHead: lastReviewedCommit };
|
|
19
|
+
}
|
|
20
|
+
if (pullRequest.state !== "closed" || !pullRequest.mergedAt || !pullRequest.mergeCommitSha ||
|
|
21
|
+
lastReviewedCommit !== pullRequest.head.sha) {
|
|
22
|
+
return { status: "failed", code: "PR_MERGE_IDENTITY_INVALID", message: "PR merge identity does not match the reviewed head" };
|
|
23
|
+
}
|
|
24
|
+
const shas = [pullRequest.base.sha, pullRequest.head.sha, pullRequest.mergeCommitSha, localHead];
|
|
25
|
+
if (shas.some((sha) => !SHA_PATTERN.test(sha) || !commitExists(gitRoot, sha))) {
|
|
26
|
+
return { status: "blocked", code: "PR_MERGE_OBJECT_MISSING", message: "Required PR merge Git objects are unavailable" };
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
git(gitRoot, ["merge-base", "--is-ancestor", pullRequest.mergeCommitSha, localHead]);
|
|
30
|
+
const parents = git(gitRoot, ["rev-list", "--parents", "-n", "1", pullRequest.mergeCommitSha]).trim().split(/\s+/);
|
|
31
|
+
if (parents.length !== 2) {
|
|
32
|
+
return { status: "failed", code: "PR_MERGE_TOPOLOGY_INVALID", message: "PR merge commit is not a single-parent squash commit" };
|
|
33
|
+
}
|
|
34
|
+
const diffArgs = ["diff", "--no-renames", "--binary", "--full-index"];
|
|
35
|
+
const reviewedDiff = git(gitRoot, [...diffArgs, pullRequest.base.sha, pullRequest.head.sha]);
|
|
36
|
+
const mergedDiff = git(gitRoot, [...diffArgs, `${pullRequest.mergeCommitSha}^`, pullRequest.mergeCommitSha]);
|
|
37
|
+
if (reviewedDiff !== mergedDiff) {
|
|
38
|
+
return { status: "failed", code: "PR_MERGE_CONTENT_MISMATCH", message: "Squash merge content differs from the reviewed PR changes" };
|
|
39
|
+
}
|
|
40
|
+
const postMerge = git(gitRoot, ["rev-list", `${pullRequest.mergeCommitSha}..${localHead}`, "--", ...pathspecs]).trim();
|
|
41
|
+
if (postMerge) {
|
|
42
|
+
return { status: "failed", code: "POST_MERGE_CHANGES", message: "Protected paths changed after the squash merge" };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return { status: "blocked", code: "PR_MERGE_GIT_EVIDENCE_UNAVAILABLE", message: "Unable to verify PR merge topology or content" };
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
status: "merged-equivalent",
|
|
50
|
+
reviewedHead: lastReviewedCommit,
|
|
51
|
+
mergeCommit: pullRequest.mergeCommitSha
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=merged-pr-equivalence.js.map
|
|
@@ -118,7 +118,7 @@ function inspectRequiredChecks(taskRef, options = {}) {
|
|
|
118
118
|
return checksResult(inspected.error.retryable ? 'blocked' : 'failed', {
|
|
119
119
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
120
120
|
resource: { kind: 'pull-request', number: base.prNumber },
|
|
121
|
-
pullRequest:
|
|
121
|
+
pullRequest: base.pullRequest, error: inspected.error
|
|
122
122
|
});
|
|
123
123
|
}
|
|
124
124
|
const classified = classifyRequiredChecks(normalizeChecks(inspected.value));
|
|
@@ -128,7 +128,7 @@ function inspectRequiredChecks(taskRef, options = {}) {
|
|
|
128
128
|
return checksResult(status, {
|
|
129
129
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
130
130
|
resource: { kind: 'pull-request', number: base.prNumber },
|
|
131
|
-
pullRequest:
|
|
131
|
+
pullRequest: base.pullRequest, checks: classified,
|
|
132
132
|
error: status === 'no-op' ? null : {
|
|
133
133
|
code: classified.state === 'pending' ? 'REQUIRED_CHECKS_PENDING' : `REQUIRED_CHECKS_${classified.state.toUpperCase()}`,
|
|
134
134
|
message: `Required checks are ${classified.state}`,
|
|
@@ -172,10 +172,10 @@ function resolvePlatformCheckRun(taskRef, options) {
|
|
|
172
172
|
if (direct) {
|
|
173
173
|
const run = base.client.json(['api', `repos/${base.context.platform.repository}/actions/runs/${direct.runId}`], { cwd: base.resolved.repoRoot });
|
|
174
174
|
if (!run.ok)
|
|
175
|
-
return checksResult(run.error.retryable ? 'blocked' : 'failed', { platform: base.context.platform, capabilities: base.context.capabilities, pullRequest:
|
|
175
|
+
return checksResult(run.error.retryable ? 'blocked' : 'failed', { platform: base.context.platform, capabilities: base.context.capabilities, pullRequest: base.pullRequest, error: run.error });
|
|
176
176
|
if (run.value.head_sha !== base.pullRequest.head.sha)
|
|
177
177
|
return checksResult('failed', { error: { code: 'CHECK_RUN_HEAD_MISMATCH', message: 'Resolved run does not belong to the PR head SHA', retryable: false } });
|
|
178
|
-
return checksResult('no-op', { platform: base.context.platform, capabilities: base.context.capabilities, resource: { kind: 'pull-request', number: base.prNumber }, pullRequest:
|
|
178
|
+
return checksResult('no-op', { platform: base.context.platform, capabilities: base.context.capabilities, resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: base.pullRequest, resolution: { status: 'resolved', ...direct }, error: null });
|
|
179
179
|
}
|
|
180
180
|
const listed = base.client.json(['api', `repos/${base.context.platform.repository}/commits/${base.pullRequest.head.sha}/check-runs`], { cwd: base.resolved.repoRoot });
|
|
181
181
|
if (!listed.ok)
|
|
@@ -189,7 +189,7 @@ function resolvePlatformCheckRun(taskRef, options) {
|
|
|
189
189
|
const resolution = resolveRunCandidate(candidates, base.pullRequest.head.sha, options.checkName);
|
|
190
190
|
return checksResult(resolution.status === 'resolved' ? 'no-op' : 'failed', {
|
|
191
191
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
192
|
-
resource: { kind: 'pull-request', number: base.prNumber }, pullRequest:
|
|
192
|
+
resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: base.pullRequest,
|
|
193
193
|
resolution,
|
|
194
194
|
error: resolution.status === 'resolved' ? null : { code: resolution.status === 'ambiguous' ? 'CHECK_RUN_AMBIGUOUS' : 'CHECK_RUN_NOT_FOUND', message: `Check run is ${resolution.status}`, retryable: false }
|
|
195
195
|
});
|
|
@@ -208,7 +208,7 @@ function fetchPlatformCheckLogs(taskRef, options) {
|
|
|
208
208
|
return checksResult('failed', { error: { code: 'CHECK_LOGS_MISSING', message: 'No failed logs are available', retryable: false } });
|
|
209
209
|
return checksResult('no-op', {
|
|
210
210
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
211
|
-
resource: { kind: 'pull-request', number: base.prNumber }, pullRequest:
|
|
211
|
+
resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: base.pullRequest,
|
|
212
212
|
logs: { runId: options.run, ...(options.job ? { jobId: options.job } : {}), text: fetched.value }, error: null
|
|
213
213
|
});
|
|
214
214
|
}
|
|
@@ -33,7 +33,9 @@ function normalizePullRequest(remote, repository) {
|
|
|
33
33
|
body: remote.body || '',
|
|
34
34
|
draft: Boolean(remote.draft),
|
|
35
35
|
head: { repository: headRepository, ref: remote.head.ref, sha: remote.head.sha },
|
|
36
|
-
base: { repository: baseRepository, ref: remote.base.ref },
|
|
36
|
+
base: { repository: baseRepository, ref: remote.base.ref, sha: remote.base.sha || '' },
|
|
37
|
+
mergedAt: remote.merged_at || null,
|
|
38
|
+
mergeCommitSha: remote.merge_commit_sha || null,
|
|
37
39
|
labels: (remote.labels || []).map((label) => typeof label === 'string' ? label : label.name || '').filter(Boolean).sort(),
|
|
38
40
|
assignees: (remote.assignees || []).map((assignee) => assignee.login || '').filter(Boolean).sort(),
|
|
39
41
|
milestone: remote.milestone?.title || null
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { resolvePlatformContext } from './context.js';
|
|
5
|
+
import { fetchGitHubReleaseNoteData, publishGitHubReleaseNotes } from './github-release-notes.js';
|
|
6
|
+
const unsupportedError = {
|
|
7
|
+
code: 'PLATFORM_RELEASE_NOTES_UNSUPPORTED',
|
|
8
|
+
message: 'Release notes are not supported for the configured platform',
|
|
9
|
+
retryable: false
|
|
10
|
+
};
|
|
11
|
+
function baseResult(status) {
|
|
12
|
+
return {
|
|
13
|
+
status,
|
|
14
|
+
changed: false,
|
|
15
|
+
range: null,
|
|
16
|
+
history: [],
|
|
17
|
+
pullRequests: [],
|
|
18
|
+
commits: [],
|
|
19
|
+
error: status === 'no-op' ? unsupportedError : null
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function git(cwd, args) {
|
|
23
|
+
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
24
|
+
}
|
|
25
|
+
function configuredPlatform(cwd) {
|
|
26
|
+
try {
|
|
27
|
+
const value = JSON.parse(fs.readFileSync(path.join(cwd, '.agents', '.airc.json'), 'utf8'));
|
|
28
|
+
return typeof value?.platform?.type === 'string' ? value.platform.type : null;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function releaseNoteContext(input, options = {}) {
|
|
35
|
+
const historyLimit = input.historyLimit ?? 3;
|
|
36
|
+
if (!input.fromTag || !input.toTag || !input.branch || !Number.isInteger(historyLimit) || historyLimit < 1 || historyLimit > 20) {
|
|
37
|
+
return { ...baseResult('failed'), error: { code: 'RELEASE_NOTES_INPUT_INVALID', message: 'from-tag, to-tag, branch, and a history limit from 1 to 20 are required', retryable: false } };
|
|
38
|
+
}
|
|
39
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
40
|
+
const platformType = options.platformType ?? configuredPlatform(cwd);
|
|
41
|
+
if (platformType !== 'github')
|
|
42
|
+
return baseResult('no-op');
|
|
43
|
+
let fromOid;
|
|
44
|
+
let toOid;
|
|
45
|
+
let fromTime;
|
|
46
|
+
let toTime;
|
|
47
|
+
let commitFacts;
|
|
48
|
+
try {
|
|
49
|
+
fromOid = git(cwd, ['rev-parse', '--verify', `${input.fromTag}^{commit}`]);
|
|
50
|
+
toOid = git(cwd, ['rev-parse', '--verify', `${input.toTag}^{commit}`]);
|
|
51
|
+
git(cwd, ['merge-base', '--is-ancestor', fromOid, toOid]);
|
|
52
|
+
fromTime = git(cwd, ['show', '-s', '--format=%cI', fromOid]);
|
|
53
|
+
toTime = git(cwd, ['show', '-s', '--format=%cI', toOid]);
|
|
54
|
+
const raw = git(cwd, ['log', '--reverse', '--no-merges', '--format=%H%x00%s', `${fromOid}..${toOid}`]);
|
|
55
|
+
commitFacts = raw ? raw.split('\n').map((line) => {
|
|
56
|
+
const [oid = '', title = ''] = line.split('\0');
|
|
57
|
+
return { oid, title };
|
|
58
|
+
}) : [];
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return { ...baseResult('failed'), error: { code: 'RELEASE_NOTES_RANGE_INVALID', message: 'The release tag range is missing or is not ancestral', retryable: false } };
|
|
62
|
+
}
|
|
63
|
+
const context = resolvePlatformContext({ cwd, platformType, client: options.client });
|
|
64
|
+
if (!context.platform.repository) {
|
|
65
|
+
return {
|
|
66
|
+
...baseResult(context.status === 'blocked' ? 'blocked' : 'failed'),
|
|
67
|
+
error: context.error || { code: 'PLATFORM_CONTEXT_UNAVAILABLE', message: 'Platform repository is unavailable', retryable: context.status === 'blocked' }
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const platform = fetchGitHubReleaseNoteData({
|
|
71
|
+
repository: context.platform.repository,
|
|
72
|
+
commitOids: commitFacts.map((item) => item.oid),
|
|
73
|
+
branch: input.branch,
|
|
74
|
+
historyLimit,
|
|
75
|
+
fromTime,
|
|
76
|
+
toTime
|
|
77
|
+
}, { cwd, client: options.client });
|
|
78
|
+
if (platform.status === 'blocked' || platform.status === 'failed' || !('authors' in platform)) {
|
|
79
|
+
return { ...baseResult(platform.status), error: platform.error };
|
|
80
|
+
}
|
|
81
|
+
const commits = commitFacts.map((commit) => {
|
|
82
|
+
const seen = new Set();
|
|
83
|
+
const authors = (platform.authors.get(commit.oid) || []).filter((actor) => {
|
|
84
|
+
const key = actor.login || actor.email?.toLowerCase() || actor.name.toLowerCase();
|
|
85
|
+
if (seen.has(key))
|
|
86
|
+
return false;
|
|
87
|
+
seen.add(key);
|
|
88
|
+
return true;
|
|
89
|
+
});
|
|
90
|
+
return { ...commit, authors };
|
|
91
|
+
});
|
|
92
|
+
const pullRequests = platform.pullRequests
|
|
93
|
+
.map((item) => ({
|
|
94
|
+
number: Number(item.number),
|
|
95
|
+
title: String(item.title || ''),
|
|
96
|
+
body: String(item.body || ''),
|
|
97
|
+
url: String(item.url || ''),
|
|
98
|
+
mergedAt: String(item.mergedAt || ''),
|
|
99
|
+
labels: Array.isArray(item.labels) ? item.labels.map((label) => String(label.name || label)) : [],
|
|
100
|
+
author: item.author && typeof item.author === 'object'
|
|
101
|
+
? { login: String(item.author.login || '').toLowerCase(), bot: String(item.author.login || '').endsWith('[bot]') }
|
|
102
|
+
: null,
|
|
103
|
+
closingIssues: Array.isArray(item.closingIssuesReferences)
|
|
104
|
+
? item.closingIssuesReferences.map((issue) => {
|
|
105
|
+
const value = issue;
|
|
106
|
+
return {
|
|
107
|
+
number: Number(value.number),
|
|
108
|
+
title: String(value.title || ''),
|
|
109
|
+
url: String(value.url || ''),
|
|
110
|
+
labels: Array.isArray(value.labels) ? value.labels.map((label) => String(label.name || label)) : [],
|
|
111
|
+
author: value.author && typeof value.author === 'object'
|
|
112
|
+
? { login: String(value.author.login || '').toLowerCase(), bot: String(value.author.login || '').endsWith('[bot]') }
|
|
113
|
+
: null
|
|
114
|
+
};
|
|
115
|
+
}) : []
|
|
116
|
+
}))
|
|
117
|
+
.sort((left, right) => left.number - right.number);
|
|
118
|
+
return {
|
|
119
|
+
status: 'no-op',
|
|
120
|
+
changed: false,
|
|
121
|
+
range: { fromTag: input.fromTag, toTag: input.toTag, fromOid, toOid },
|
|
122
|
+
history: platform.history,
|
|
123
|
+
pullRequests,
|
|
124
|
+
commits,
|
|
125
|
+
error: null
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function publishReleaseNotes(input, options = {}) {
|
|
129
|
+
if (!input.tag || !input.title || !input.notesFile) {
|
|
130
|
+
return { status: 'failed', changed: false, operation: null, url: null, error: { code: 'RELEASE_NOTES_INPUT_INVALID', message: 'tag, title, and notes-file are required', retryable: false } };
|
|
131
|
+
}
|
|
132
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
133
|
+
const platformType = options.platformType ?? configuredPlatform(cwd);
|
|
134
|
+
if (platformType !== 'github')
|
|
135
|
+
return { status: 'no-op', changed: false, operation: null, url: null, error: unsupportedError };
|
|
136
|
+
const context = resolvePlatformContext({ cwd, platformType, client: options.client });
|
|
137
|
+
if (!context.platform.repository) {
|
|
138
|
+
return { status: context.status === 'blocked' ? 'blocked' : 'failed', changed: false, operation: null, url: null, error: context.error };
|
|
139
|
+
}
|
|
140
|
+
return publishGitHubReleaseNotes({ ...input, repository: context.platform.repository }, { cwd, client: options.client });
|
|
141
|
+
}
|
|
142
|
+
export { publishReleaseNotes, releaseNoteContext };
|
|
143
|
+
//# sourceMappingURL=release-notes.js.map
|