@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
|
@@ -4,9 +4,11 @@ import path from "node:path";
|
|
|
4
4
|
import process from "node:process";
|
|
5
5
|
import { execFileSync } from "node:child_process";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { extractReviewBaseline, extractReviewDiffFingerprint, extractReviewedSnapshotTree, findAuthoritativeReviewCodeArtifact, parseReviewVerdict, resolvePostReviewGlobs } from "./verification-review.js";
|
|
7
|
+
import { extractReviewBaseline, extractReviewDiffFingerprint, extractReviewedSnapshotTree, findAuthoritativeReviewCodeArtifact, loadPostReviewConfig, parseReviewVerdict, resolvePostReviewGlobs } from "./verification-review.js";
|
|
8
8
|
import { check as checkPlatformSync } from "../platform/verification-sync.js";
|
|
9
9
|
import { check as checkRequiredChecks } from "../platform/verification-required.js";
|
|
10
|
+
import { inspectPlatformPullRequest } from "../platform/pull-requests.js";
|
|
11
|
+
import { resolveReviewedHeadRelation } from "../platform/merged-pr-equivalence.js";
|
|
10
12
|
import { parseTypedTaskFrontmatter } from "./frontmatter.js";
|
|
11
13
|
import { parseLedger } from "./ledger.js";
|
|
12
14
|
import { loadVerificationConfig } from "./verification-config.js";
|
|
@@ -210,19 +212,6 @@ function loadProjectName() {
|
|
|
210
212
|
return "";
|
|
211
213
|
}
|
|
212
214
|
}
|
|
213
|
-
function loadReviewConfig() {
|
|
214
|
-
const configPath = path.join(repoRoot, ".agents", ".airc.json");
|
|
215
|
-
if (!fs.existsSync(configPath)) {
|
|
216
|
-
return {};
|
|
217
|
-
}
|
|
218
|
-
try {
|
|
219
|
-
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
220
|
-
return config.review && typeof config.review === "object" ? config.review : {};
|
|
221
|
-
}
|
|
222
|
-
catch {
|
|
223
|
-
return {};
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
215
|
function checkArtifact({ taskDir, config, artifactFile }) {
|
|
227
216
|
const resolvedArtifact = resolveArtifactPath(taskDir, config.file_pattern, artifactFile);
|
|
228
217
|
if (!resolvedArtifact.ok) {
|
|
@@ -495,7 +484,7 @@ function resolveReviewSetting(config, key, fallback) {
|
|
|
495
484
|
if (config && config[key] !== undefined && config[key] !== null) {
|
|
496
485
|
return config[key];
|
|
497
486
|
}
|
|
498
|
-
const reviewConfig =
|
|
487
|
+
const reviewConfig = loadPostReviewConfig(repoRoot);
|
|
499
488
|
if (reviewConfig[key] !== undefined && reviewConfig[key] !== null) {
|
|
500
489
|
return reviewConfig[key];
|
|
501
490
|
}
|
|
@@ -590,7 +579,7 @@ function checkPostReviewCommit({ taskDir, config }) {
|
|
|
590
579
|
return baselineSource.result;
|
|
591
580
|
}
|
|
592
581
|
const sha = baselineSource.sha;
|
|
593
|
-
const globs = resolvePostReviewGlobs(config,
|
|
582
|
+
const globs = resolvePostReviewGlobs(config, loadPostReviewConfig(repoRoot));
|
|
594
583
|
let commits;
|
|
595
584
|
try {
|
|
596
585
|
const out = execFileSync("git", ["-C", gitRoot, "rev-list", `${sha}..HEAD`, "--", ...globs], { encoding: "utf8" });
|
|
@@ -602,6 +591,34 @@ function checkPostReviewCommit({ taskDir, config }) {
|
|
|
602
591
|
if (commits.length === 0) {
|
|
603
592
|
return passResult("post-review-commit", `No post-review commits to code/rule paths since ${sha.slice(0, 8)}`);
|
|
604
593
|
}
|
|
594
|
+
if (task.ok && Number(task.metadata.pr_number) > 0) {
|
|
595
|
+
const inspected = inspectPlatformPullRequest(task.metadata.id, { cwd: gitRoot });
|
|
596
|
+
if (inspected.pullRequest) {
|
|
597
|
+
let localHead = "";
|
|
598
|
+
try {
|
|
599
|
+
localHead = execFileSync("git", ["-C", gitRoot, "rev-parse", "HEAD"], { encoding: "utf8" }).trim();
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
return blockedResult("post-review-commit", "Unable to resolve the local HEAD");
|
|
603
|
+
}
|
|
604
|
+
const relation = resolveReviewedHeadRelation({
|
|
605
|
+
gitRoot,
|
|
606
|
+
localHead,
|
|
607
|
+
lastReviewedCommit: sha,
|
|
608
|
+
pullRequest: inspected.pullRequest,
|
|
609
|
+
pathspecs: globs
|
|
610
|
+
});
|
|
611
|
+
if (relation.status === "merged-equivalent") {
|
|
612
|
+
return passResult("post-review-commit", `Reviewed PR head is content-equivalent to squash merge ${relation.mergeCommit.slice(0, 8)}`);
|
|
613
|
+
}
|
|
614
|
+
if (relation.status === "blocked") {
|
|
615
|
+
return blockedResult("post-review-commit", relation.message);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
else if (inspected.status === "blocked") {
|
|
619
|
+
return blockedResult("post-review-commit", inspected.error?.message || "PR merge snapshot is unavailable");
|
|
620
|
+
}
|
|
621
|
+
}
|
|
605
622
|
let exempt = false;
|
|
606
623
|
try {
|
|
607
624
|
exempt = task.ok && parseLedger(task.content).some((row) => row.stage === POST_REVIEW_COMMIT_STAGE && row.status === "human-decided");
|
|
@@ -647,7 +664,7 @@ function checkReviewFact({ taskDir, artifactFile }) {
|
|
|
647
664
|
}
|
|
648
665
|
let actualSnapshot;
|
|
649
666
|
try {
|
|
650
|
-
actualSnapshot = snapshotReview({ cwd: gitRoot, mode: "worktree", baseline, globs: resolvePostReviewGlobs({},
|
|
667
|
+
actualSnapshot = snapshotReview({ cwd: gitRoot, mode: "worktree", baseline, globs: resolvePostReviewGlobs({}, loadPostReviewConfig(repoRoot)) });
|
|
651
668
|
}
|
|
652
669
|
catch {
|
|
653
670
|
return blockedResult("review-fact", `Unable to recompute reviewed diff fingerprint from baseline ${baseline.slice(0, 8)}; re-run review-code`);
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { extractReviewBaseline, extractReviewDiffFingerprint, extractReviewedSnapshotTree, findAuthoritativeReviewCodeArtifact, parseReviewVerdict, resolvePostReviewGlobs } from './review-fingerprint.js';
|
|
1
|
+
export { extractReviewBaseline, extractReviewDiffFingerprint, extractReviewedSnapshotTree, findAuthoritativeReviewCodeArtifact, loadPostReviewConfig, parseReviewVerdict, resolvePostReviewGlobs } from './review-fingerprint.js';
|
|
2
2
|
export { parseLedger, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
|
|
3
3
|
//# sourceMappingURL=verification-review.js.map
|
|
@@ -19,7 +19,7 @@ const VERIFICATION_CATALOG = {
|
|
|
19
19
|
'block-task.completed': gate('block-task', 'blocked'),
|
|
20
20
|
'cancel-task.completed': gate('cancel-task', 'completed'),
|
|
21
21
|
'commit.completed': gate('commit', 'active'),
|
|
22
|
-
'complete-task.preflight': { skill: 'complete-task', expectedState: 'active', mode: 'checks', checks: ['review-ledger', 'post-review-commit', '
|
|
22
|
+
'complete-task.preflight': { skill: 'complete-task', expectedState: 'active', mode: 'checks', checks: ['review-ledger', 'post-review-commit', 'platform-sync-preflight'] },
|
|
23
23
|
'complete-task.completed': gate('complete-task', 'completed'),
|
|
24
24
|
'create-pr.completed': gate('create-pr', 'active'),
|
|
25
25
|
'create-task.completed': gate('create-task', 'active'),
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { publishReleaseNotes, releaseNoteContext } from '../platform/release-notes.ts';
|
|
5
|
+
|
|
6
|
+
function option(args: string[], name: string): string | null {
|
|
7
|
+
const index = args.indexOf(name);
|
|
8
|
+
return index >= 0 && args[index + 1] ? args[index + 1]! : null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function finish(result: { status: string; [key: string]: unknown }): void {
|
|
12
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
13
|
+
process.exitCode = result.status === 'blocked' ? 2 : result.status === 'failed' ? 1 : 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function platformReleaseNotes(args: string[] = []): void {
|
|
17
|
+
const [action, ...rest] = args;
|
|
18
|
+
const cwd = path.resolve(option(rest, '--cwd') || process.cwd());
|
|
19
|
+
if (action === 'context') {
|
|
20
|
+
const historyRaw = option(rest, '--history-limit');
|
|
21
|
+
finish(releaseNoteContext({
|
|
22
|
+
fromTag: option(rest, '--from-tag') || '',
|
|
23
|
+
toTag: option(rest, '--to-tag') || '',
|
|
24
|
+
branch: option(rest, '--branch') || '',
|
|
25
|
+
historyLimit: historyRaw === null ? undefined : Number(historyRaw)
|
|
26
|
+
}, { cwd }));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (action === 'publish') {
|
|
30
|
+
const notesPath = option(rest, '--notes-file');
|
|
31
|
+
if (!notesPath) {
|
|
32
|
+
finish({ status: 'failed', changed: false, error: { code: 'RELEASE_NOTES_INPUT_INVALID', message: 'notes-file is required', retryable: false } });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
let resolvedNotesPath = notesPath;
|
|
36
|
+
if (notesPath === '-') {
|
|
37
|
+
resolvedNotesPath = path.join(process.env.TMPDIR || '/tmp', `agent-infra-release-notes-${process.pid}.md`);
|
|
38
|
+
fs.writeFileSync(resolvedNotesPath, fs.readFileSync(0));
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
finish(publishReleaseNotes({
|
|
42
|
+
tag: option(rest, '--tag') || '',
|
|
43
|
+
title: option(rest, '--title') || option(rest, '--tag') || '',
|
|
44
|
+
notesFile: resolvedNotesPath,
|
|
45
|
+
dryRun: rest.includes('--dry-run')
|
|
46
|
+
}, { cwd }));
|
|
47
|
+
} finally {
|
|
48
|
+
if (notesPath === '-') fs.rmSync(resolvedNotesPath, { force: true });
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
finish({ status: 'failed', changed: false, error: { code: 'RELEASE_NOTES_INPUT_INVALID', message: 'Usage: platform-release-notes context|publish', retryable: false } });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { platformReleaseNotes };
|
|
@@ -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
|
|
|
@@ -17,12 +18,101 @@ type CommandResult = ReturnType<typeof command>;
|
|
|
17
18
|
type CommandRunner = (cwd: string, executable: string, args: string[]) => CommandResult;
|
|
18
19
|
type DemoResult = {
|
|
19
20
|
status: 'recorded' | 'skipped' | 'failed';
|
|
20
|
-
reasonCode: '
|
|
21
|
+
reasonCode: 'DEMO_INPUTS_UNCHANGED' | 'GIT_LFS_MISSING' | 'VHS_MISSING' | 'FFMPEG_MISSING'
|
|
22
|
+
| 'DEMO_COMMAND_FAILED' | 'DEMO_OUTPUT_MISSING' | 'DEMO_OUTPUT_INVALID'
|
|
23
|
+
| 'DEMO_OUTPUT_TOO_LARGE' | 'DEMO_DIGEST_FAILED' | null;
|
|
21
24
|
message: string | null;
|
|
22
25
|
outputPath: string | null;
|
|
23
26
|
};
|
|
24
27
|
|
|
28
|
+
const DEMO_INPUT_PATHS = [
|
|
29
|
+
'assets/demo-init.tape',
|
|
30
|
+
'scripts/demo-regen.sh',
|
|
31
|
+
'scripts/normalize-gif-duration.py',
|
|
32
|
+
'bin/cli.ts',
|
|
33
|
+
'lib/init.ts',
|
|
34
|
+
'lib/log.ts',
|
|
35
|
+
'lib/prompt.ts',
|
|
36
|
+
'lib/paths.ts',
|
|
37
|
+
'lib/render.ts',
|
|
38
|
+
'lib/builtin-tuis.ts',
|
|
39
|
+
'lib/sandbox/engines/',
|
|
40
|
+
'src/sync-templates.js',
|
|
41
|
+
'templates/'
|
|
42
|
+
] as const;
|
|
43
|
+
const DEMO_DIGEST_PATH = 'assets/demo-init.inputs.sha256';
|
|
44
|
+
const DEMO_OUTPUT_PATH = 'assets/demo-init.gif';
|
|
45
|
+
const DEMO_MAX_BYTES = 4 * 1024 * 1024;
|
|
46
|
+
|
|
47
|
+
function demoInputFiles(cwd: string): string[] {
|
|
48
|
+
const files = new Set<string>();
|
|
49
|
+
for (const input of DEMO_INPUT_PATHS) {
|
|
50
|
+
if (input.endsWith('/')) {
|
|
51
|
+
const listed = command(cwd, 'git', ['ls-files', '--', input]);
|
|
52
|
+
if (listed.status !== 0) throw new Error(String(listed.stderr || `Unable to list ${input}`));
|
|
53
|
+
const directoryFiles = String(listed.stdout).split('\n').filter(Boolean);
|
|
54
|
+
if (!directoryFiles.length) throw new Error(`Canonical demo input directory is empty: ${input}`);
|
|
55
|
+
for (const file of directoryFiles) files.add(file.replaceAll('\\', '/'));
|
|
56
|
+
} else {
|
|
57
|
+
files.add(input);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const sorted = [...files].sort((left, right) => Buffer.from(left).compare(Buffer.from(right)));
|
|
61
|
+
if (!sorted.length) throw new Error('Canonical demo input set is empty');
|
|
62
|
+
for (const file of sorted) {
|
|
63
|
+
const absolute = path.join(cwd, file);
|
|
64
|
+
if (!fs.statSync(absolute).isFile()) throw new Error(`Canonical demo input is not a file: ${file}`);
|
|
65
|
+
}
|
|
66
|
+
return sorted;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function computeDemoInputDigest(cwd: string): string {
|
|
70
|
+
const hash = crypto.createHash('sha256');
|
|
71
|
+
for (const file of demoInputFiles(cwd)) {
|
|
72
|
+
const bytes = fs.readFileSync(path.join(cwd, file));
|
|
73
|
+
hash.update(file);
|
|
74
|
+
hash.update('\0');
|
|
75
|
+
hash.update(String(bytes.byteLength));
|
|
76
|
+
hash.update('\0');
|
|
77
|
+
hash.update(bytes);
|
|
78
|
+
hash.update('\0');
|
|
79
|
+
}
|
|
80
|
+
return hash.digest('hex');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function validGif(filePath: string): boolean {
|
|
84
|
+
if (!fs.existsSync(filePath)) return false;
|
|
85
|
+
const header = fs.readFileSync(filePath).subarray(0, 6).toString('ascii');
|
|
86
|
+
return header === 'GIF87a' || header === 'GIF89a';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function writeDigestAtomically(cwd: string, digest: string): void {
|
|
90
|
+
const target = path.join(cwd, DEMO_DIGEST_PATH);
|
|
91
|
+
const temporary = `${target}.tmp-${process.pid}`;
|
|
92
|
+
fs.writeFileSync(temporary, `${digest}\n`);
|
|
93
|
+
fs.renameSync(temporary, target);
|
|
94
|
+
}
|
|
95
|
+
|
|
25
96
|
function runOptionalDemo(cwd: string, run: CommandRunner = command): DemoResult {
|
|
97
|
+
let digest: string;
|
|
98
|
+
try {
|
|
99
|
+
digest = computeDemoInputDigest(cwd);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
return { status: 'failed', reasonCode: 'DEMO_DIGEST_FAILED', message: String(error), outputPath: null };
|
|
102
|
+
}
|
|
103
|
+
const digestPath = path.join(cwd, DEMO_DIGEST_PATH);
|
|
104
|
+
const priorDigest = fs.existsSync(digestPath) ? fs.readFileSync(digestPath, 'utf8').trim() : '';
|
|
105
|
+
if (/^[0-9a-f]{64}$/.test(priorDigest) && priorDigest === digest) {
|
|
106
|
+
return { status: 'skipped', reasonCode: 'DEMO_INPUTS_UNCHANGED', message: null, outputPath: null };
|
|
107
|
+
}
|
|
108
|
+
const lfs = run(cwd, 'git', ['lfs', 'version']);
|
|
109
|
+
if (lfs.status !== 0) {
|
|
110
|
+
return { status: 'failed', reasonCode: 'GIT_LFS_MISSING', message: String(lfs.stderr || lfs.stdout), outputPath: null };
|
|
111
|
+
}
|
|
112
|
+
const lfsAttribute = run(cwd, 'git', ['check-attr', 'filter', '--', DEMO_OUTPUT_PATH]);
|
|
113
|
+
if (lfsAttribute.status !== 0 || !String(lfsAttribute.stdout).trim().endsWith(': lfs')) {
|
|
114
|
+
return { status: 'failed', reasonCode: 'GIT_LFS_MISSING', message: `${DEMO_OUTPUT_PATH} is not tracked by Git LFS`, outputPath: null };
|
|
115
|
+
}
|
|
26
116
|
const vhs = run(cwd, 'vhs', ['--version']);
|
|
27
117
|
if (vhs.status !== 0) return { status: 'skipped', reasonCode: 'VHS_MISSING', message: null, outputPath: null };
|
|
28
118
|
const ffmpeg = run(cwd, 'ffmpeg', ['-version']);
|
|
@@ -36,7 +126,7 @@ function runOptionalDemo(cwd: string, run: CommandRunner = command): DemoResult
|
|
|
36
126
|
outputPath: null
|
|
37
127
|
};
|
|
38
128
|
}
|
|
39
|
-
const outputPath =
|
|
129
|
+
const outputPath = DEMO_OUTPUT_PATH;
|
|
40
130
|
if (!fs.existsSync(path.join(cwd, outputPath))) {
|
|
41
131
|
return {
|
|
42
132
|
status: 'failed',
|
|
@@ -45,6 +135,18 @@ function runOptionalDemo(cwd: string, run: CommandRunner = command): DemoResult
|
|
|
45
135
|
outputPath: null
|
|
46
136
|
};
|
|
47
137
|
}
|
|
138
|
+
const absoluteOutput = path.join(cwd, outputPath);
|
|
139
|
+
if (!validGif(absoluteOutput)) {
|
|
140
|
+
return { status: 'failed', reasonCode: 'DEMO_OUTPUT_INVALID', message: `${outputPath} is not a GIF`, outputPath: null };
|
|
141
|
+
}
|
|
142
|
+
if (fs.statSync(absoluteOutput).size > DEMO_MAX_BYTES) {
|
|
143
|
+
return { status: 'failed', reasonCode: 'DEMO_OUTPUT_TOO_LARGE', message: `${outputPath} exceeds 4 MiB`, outputPath: null };
|
|
144
|
+
}
|
|
145
|
+
const pointer = run(cwd, 'git', ['lfs', 'pointer', `--file=${outputPath}`]);
|
|
146
|
+
if (pointer.status !== 0 || !String(pointer.stdout).includes(`size ${fs.statSync(absoluteOutput).size}`)) {
|
|
147
|
+
return { status: 'failed', reasonCode: 'DEMO_OUTPUT_INVALID', message: 'Git LFS could not produce a matching pointer', outputPath: null };
|
|
148
|
+
}
|
|
149
|
+
writeDigestAtomically(cwd, digest);
|
|
48
150
|
return { status: 'recorded', reasonCode: null, message: null, outputPath };
|
|
49
151
|
}
|
|
50
152
|
|
|
@@ -182,5 +284,5 @@ async function releaseWorkflow(args: string[] = []): Promise<void> {
|
|
|
182
284
|
process.stdout.write(`${JSON.stringify({ ...pushed, demo, snapshot: releaseSnapshot(version, await inspectFacts(cwd, version)) })}\n`); process.exitCode = pushed.status === 'failed' ? 1 : pushed.status === 'degraded' ? 2 : 0;
|
|
183
285
|
}
|
|
184
286
|
|
|
185
|
-
export { inspectFacts, releaseWorkflow, runOptionalDemo };
|
|
287
|
+
export { computeDemoInputDigest, inspectFacts, releaseWorkflow, runOptionalDemo };
|
|
186
288
|
export type { CommandRunner, DemoResult };
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { GitHubClient } from './github-client.ts';
|
|
2
|
+
import { createGitHubClient } from './github-client.ts';
|
|
3
|
+
|
|
4
|
+
type GitHubActorInput = {
|
|
5
|
+
name?: string | null;
|
|
6
|
+
email?: string | null;
|
|
7
|
+
user?: { login?: string | null } | null;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type ReleaseNoteActor = {
|
|
11
|
+
name: string;
|
|
12
|
+
email: string | null;
|
|
13
|
+
login: string | null;
|
|
14
|
+
bot: boolean;
|
|
15
|
+
resolution: 'platform-user' | 'platform-noreply' | 'unresolved';
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type GitHubReleaseNoteOptions = { cwd?: string; client?: GitHubClient };
|
|
19
|
+
|
|
20
|
+
function normalizeGitHubActor(actor: GitHubActorInput): ReleaseNoteActor {
|
|
21
|
+
const name = String(actor.name || '').trim();
|
|
22
|
+
const email = actor.email ? String(actor.email).trim() : null;
|
|
23
|
+
const platformLogin = actor.user?.login?.trim().toLowerCase() || null;
|
|
24
|
+
const noReplyLogin = email
|
|
25
|
+
? /^(?:\d+\+)?([^@]+)@users\.noreply\.github\.com$/i.exec(email)?.[1]?.toLowerCase() || null
|
|
26
|
+
: null;
|
|
27
|
+
const login = platformLogin || noReplyLogin;
|
|
28
|
+
return {
|
|
29
|
+
name,
|
|
30
|
+
email,
|
|
31
|
+
login,
|
|
32
|
+
bot: Boolean(login?.endsWith('[bot]')),
|
|
33
|
+
resolution: platformLogin ? 'platform-user' : noReplyLogin ? 'platform-noreply' : 'unresolved'
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function failure(error: { code: string; message: string; retryable: boolean }) {
|
|
38
|
+
return {
|
|
39
|
+
status: error.retryable ? 'blocked' as const : 'failed' as const,
|
|
40
|
+
changed: false,
|
|
41
|
+
operation: null,
|
|
42
|
+
url: null,
|
|
43
|
+
error
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function publishGitHubReleaseNotes(
|
|
48
|
+
input: { repository: string; tag: string; title: string; notesFile: string; dryRun?: boolean },
|
|
49
|
+
options: GitHubReleaseNoteOptions = {}
|
|
50
|
+
) {
|
|
51
|
+
const client = options.client ?? createGitHubClient();
|
|
52
|
+
const inspected = client.json<{ url?: string }>(
|
|
53
|
+
['release', 'view', input.tag, '--repo', input.repository, '--json', 'url'],
|
|
54
|
+
{ cwd: options.cwd }
|
|
55
|
+
);
|
|
56
|
+
if (!inspected.ok && inspected.error.code !== 'RESOURCE_NOT_FOUND') return failure(inspected.error);
|
|
57
|
+
const exists = inspected.ok;
|
|
58
|
+
const operation = exists ? 'release:update-notes' as const : 'release:create' as const;
|
|
59
|
+
if (input.dryRun) {
|
|
60
|
+
return {
|
|
61
|
+
status: 'planned' as const,
|
|
62
|
+
changed: false,
|
|
63
|
+
operation,
|
|
64
|
+
url: inspected.ok ? inspected.value.url || null : null,
|
|
65
|
+
error: null
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const args = exists
|
|
69
|
+
? ['release', 'edit', input.tag, '--repo', input.repository, '--notes-file', input.notesFile]
|
|
70
|
+
: ['release', 'create', input.tag, '--repo', input.repository, '--title', input.title, '--notes-file', input.notesFile];
|
|
71
|
+
const written = client.text(args, { cwd: options.cwd, method: exists ? 'PATCH' : 'POST' });
|
|
72
|
+
if (!written.ok) return failure(written.error);
|
|
73
|
+
return {
|
|
74
|
+
status: 'applied' as const,
|
|
75
|
+
changed: true,
|
|
76
|
+
operation,
|
|
77
|
+
url: written.value || (inspected.ok ? inspected.value.url || null : null),
|
|
78
|
+
error: null
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function fetchGitHubReleaseNoteData(
|
|
83
|
+
input: { repository: string; commitOids: string[]; branch: string; historyLimit: number; fromTime: string; toTime: string },
|
|
84
|
+
options: GitHubReleaseNoteOptions = {}
|
|
85
|
+
) {
|
|
86
|
+
const client = options.client ?? createGitHubClient();
|
|
87
|
+
const releases = client.json<Array<{ tagName?: string; isDraft?: boolean; isPrerelease?: boolean }>>(
|
|
88
|
+
['release', 'list', '--repo', input.repository, '--limit', String(input.historyLimit + 10), '--json', 'tagName,isDraft,isPrerelease'],
|
|
89
|
+
{ cwd: options.cwd }
|
|
90
|
+
);
|
|
91
|
+
if (!releases.ok) return failure(releases.error);
|
|
92
|
+
const history = [];
|
|
93
|
+
for (const item of releases.value.filter((entry) => !entry.isDraft && !entry.isPrerelease).slice(0, input.historyLimit)) {
|
|
94
|
+
const viewed = client.json<{ body?: string; url?: string }>(
|
|
95
|
+
['release', 'view', String(item.tagName), '--repo', input.repository, '--json', 'body,url'],
|
|
96
|
+
{ cwd: options.cwd }
|
|
97
|
+
);
|
|
98
|
+
if (!viewed.ok) return failure(viewed.error);
|
|
99
|
+
history.push({ tag: String(item.tagName), body: String(viewed.value.body || ''), url: viewed.value.url || null });
|
|
100
|
+
}
|
|
101
|
+
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,closingIssuesReferences'],
|
|
103
|
+
{ cwd: options.cwd }
|
|
104
|
+
);
|
|
105
|
+
if (!prs.ok) return failure(prs.error);
|
|
106
|
+
const authors = new Map<string, ReleaseNoteActor[]>();
|
|
107
|
+
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}}}}}}`;
|
|
109
|
+
const [owner, name] = input.repository.split('/');
|
|
110
|
+
const result = client.json<{
|
|
111
|
+
data?: { repository?: { object?: { authors?: { nodes?: GitHubActorInput[]; pageInfo?: { hasNextPage?: boolean } } } } };
|
|
112
|
+
}>([
|
|
113
|
+
'api', 'graphql', '-f', `query=${query}`, '-F', `owner=${owner}`, '-F', `name=${name}`, '-F', `oid=${oid}`
|
|
114
|
+
], { cwd: options.cwd });
|
|
115
|
+
if (!result.ok) return failure(result.error);
|
|
116
|
+
const connection = result.value.data?.repository?.object?.authors;
|
|
117
|
+
if (connection?.pageInfo?.hasNextPage) {
|
|
118
|
+
return failure({ code: 'RELEASE_NOTES_AUTHORS_TRUNCATED', message: `Commit authors exceeded the supported page size for ${oid}`, retryable: false });
|
|
119
|
+
}
|
|
120
|
+
authors.set(oid, (connection?.nodes || []).map(normalizeGitHubActor));
|
|
121
|
+
}
|
|
122
|
+
const fromTime = Date.parse(input.fromTime);
|
|
123
|
+
const toTime = Date.parse(input.toTime);
|
|
124
|
+
const pullRequests = prs.value.filter((item) => {
|
|
125
|
+
const mergedAt = Date.parse(String(item.mergedAt || ''));
|
|
126
|
+
return Number.isFinite(mergedAt) && mergedAt > fromTime && mergedAt <= toTime;
|
|
127
|
+
});
|
|
128
|
+
return { status: 'no-op' as const, changed: false, history, pullRequests, authors, error: null };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export { fetchGitHubReleaseNoteData, normalizeGitHubActor, publishGitHubReleaseNotes };
|
|
132
|
+
export type { ReleaseNoteActor };
|
package/lib/platform/issues.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { buildDefaultBody } from '../task/commands/issue-body.ts';
|
|
|
6
6
|
import { extractTitle, parseTaskFrontmatter } from '../task/frontmatter.ts';
|
|
7
7
|
import { requirementFieldLabels, renderTemplateBody } from '../task/issue-form.ts';
|
|
8
8
|
import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
9
|
-
import { extractSection } from '../task/sections.ts';
|
|
9
|
+
import { extractSection, findSectionHeading } from '../task/sections.ts';
|
|
10
10
|
import { writeTask } from '../task/write.ts';
|
|
11
11
|
import { resolvePlatformContext } from './context.ts';
|
|
12
12
|
import { createGitHubClient } from './github-client.ts';
|
|
@@ -308,9 +308,11 @@ function deterministicIssueBody(repoRoot: string, content: string, taskType: str
|
|
|
308
308
|
if (selected) {
|
|
309
309
|
try {
|
|
310
310
|
return renderTemplateBody(fs.readFileSync(selected, 'utf8'), {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
311
|
+
title: taskTitle(content),
|
|
312
|
+
description: extractSection(content, ['描述', 'Description']),
|
|
313
|
+
requirements: extractSection(content, ['需求', 'Requirements']),
|
|
314
|
+
taskInput: extractSection(content, ['任务输入', 'Task Input']),
|
|
315
|
+
taskInputHeading: findSectionHeading(content, ['任务输入', 'Task Input'])
|
|
314
316
|
});
|
|
315
317
|
} catch {
|
|
316
318
|
// Invalid or unavailable forms fall back to the deterministic default body.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
import type { PullRequestSnapshot } from "./pull-requests.ts";
|
|
4
|
+
|
|
5
|
+
const SHA_PATTERN = /^[0-9a-f]{40}$/i;
|
|
6
|
+
|
|
7
|
+
type ReviewedHeadRelation =
|
|
8
|
+
| { status: "strict"; reviewedHead: string }
|
|
9
|
+
| { status: "merged-equivalent"; reviewedHead: string; mergeCommit: string }
|
|
10
|
+
| { status: "failed" | "blocked"; code: string; message: string };
|
|
11
|
+
|
|
12
|
+
function git(cwd: string, args: string[]): string {
|
|
13
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function commitExists(gitRoot: string, sha: string): boolean {
|
|
17
|
+
try {
|
|
18
|
+
git(gitRoot, ["cat-file", "-e", `${sha}^{commit}`]);
|
|
19
|
+
return true;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolveReviewedHeadRelation(input: {
|
|
26
|
+
gitRoot: string;
|
|
27
|
+
localHead: string;
|
|
28
|
+
lastReviewedCommit: string;
|
|
29
|
+
pullRequest: PullRequestSnapshot;
|
|
30
|
+
pathspecs: string[];
|
|
31
|
+
}): ReviewedHeadRelation {
|
|
32
|
+
const { gitRoot, localHead, lastReviewedCommit, pullRequest, pathspecs } = input;
|
|
33
|
+
if (localHead === lastReviewedCommit && lastReviewedCommit === pullRequest.head.sha) {
|
|
34
|
+
return { status: "strict", reviewedHead: lastReviewedCommit };
|
|
35
|
+
}
|
|
36
|
+
if (pullRequest.state !== "closed" || !pullRequest.mergedAt || !pullRequest.mergeCommitSha ||
|
|
37
|
+
lastReviewedCommit !== pullRequest.head.sha) {
|
|
38
|
+
return { status: "failed", code: "PR_MERGE_IDENTITY_INVALID", message: "PR merge identity does not match the reviewed head" };
|
|
39
|
+
}
|
|
40
|
+
const shas = [pullRequest.base.sha, pullRequest.head.sha, pullRequest.mergeCommitSha, localHead];
|
|
41
|
+
if (shas.some((sha) => !SHA_PATTERN.test(sha) || !commitExists(gitRoot, sha))) {
|
|
42
|
+
return { status: "blocked", code: "PR_MERGE_OBJECT_MISSING", message: "Required PR merge Git objects are unavailable" };
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
git(gitRoot, ["merge-base", "--is-ancestor", pullRequest.mergeCommitSha, localHead]);
|
|
46
|
+
const parents = git(gitRoot, ["rev-list", "--parents", "-n", "1", pullRequest.mergeCommitSha]).trim().split(/\s+/);
|
|
47
|
+
if (parents.length !== 2) {
|
|
48
|
+
return { status: "failed", code: "PR_MERGE_TOPOLOGY_INVALID", message: "PR merge commit is not a single-parent squash commit" };
|
|
49
|
+
}
|
|
50
|
+
const diffArgs = ["diff", "--no-renames", "--binary", "--full-index"];
|
|
51
|
+
const reviewedDiff = git(gitRoot, [...diffArgs, pullRequest.base.sha, pullRequest.head.sha]);
|
|
52
|
+
const mergedDiff = git(gitRoot, [...diffArgs, `${pullRequest.mergeCommitSha}^`, pullRequest.mergeCommitSha]);
|
|
53
|
+
if (reviewedDiff !== mergedDiff) {
|
|
54
|
+
return { status: "failed", code: "PR_MERGE_CONTENT_MISMATCH", message: "Squash merge content differs from the reviewed PR changes" };
|
|
55
|
+
}
|
|
56
|
+
const postMerge = git(gitRoot, ["rev-list", `${pullRequest.mergeCommitSha}..${localHead}`, "--", ...pathspecs]).trim();
|
|
57
|
+
if (postMerge) {
|
|
58
|
+
return { status: "failed", code: "POST_MERGE_CHANGES", message: "Protected paths changed after the squash merge" };
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
return { status: "blocked", code: "PR_MERGE_GIT_EVIDENCE_UNAVAILABLE", message: "Unable to verify PR merge topology or content" };
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
status: "merged-equivalent",
|
|
65
|
+
reviewedHead: lastReviewedCommit,
|
|
66
|
+
mergeCommit: pullRequest.mergeCommitSha
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type { ReviewedHeadRelation };
|
|
@@ -8,6 +8,7 @@ import type { GitHubClient } from './github-client.ts';
|
|
|
8
8
|
import { inspectPlatformPullRequest } from './pull-requests.ts';
|
|
9
9
|
import { platformResult } from './types.ts';
|
|
10
10
|
import type { PlatformResult } from './types.ts';
|
|
11
|
+
import type { PullRequestSnapshot } from './pull-requests.ts';
|
|
11
12
|
|
|
12
13
|
type CheckBucket = 'pass' | 'fail' | 'pending' | 'cancel';
|
|
13
14
|
type CheckState = 'passed' | 'failed' | 'pending' | 'timed-out' | 'cancelled' | 'no-required';
|
|
@@ -23,7 +24,7 @@ type CheckSnapshot = {
|
|
|
23
24
|
type ChecksSnapshot = { state: Exclude<CheckState, 'timed-out'>; required: CheckSnapshot[] };
|
|
24
25
|
type RunCandidate = { id: number; name: string; headSha: string; jobId?: number | null };
|
|
25
26
|
type ChecksResult = PlatformResult & {
|
|
26
|
-
pullRequest:
|
|
27
|
+
pullRequest: PullRequestSnapshot | null;
|
|
27
28
|
checks: { state: CheckState; required: CheckSnapshot[] };
|
|
28
29
|
resolution?: { status: 'resolved' | 'missing' | 'ambiguous'; runId: number | null; jobId: number | null };
|
|
29
30
|
logs?: { runId: number; jobId?: number; text: string };
|
|
@@ -140,7 +141,7 @@ function inspectRequiredChecks(taskRef: string, options: SharedOptions = {}): Ch
|
|
|
140
141
|
return checksResult(inspected.error.retryable ? 'blocked' : 'failed', {
|
|
141
142
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
142
143
|
resource: { kind: 'pull-request', number: base.prNumber },
|
|
143
|
-
pullRequest:
|
|
144
|
+
pullRequest: base.pullRequest, error: inspected.error
|
|
144
145
|
});
|
|
145
146
|
}
|
|
146
147
|
const classified = classifyRequiredChecks(normalizeChecks(inspected.value));
|
|
@@ -150,7 +151,7 @@ function inspectRequiredChecks(taskRef: string, options: SharedOptions = {}): Ch
|
|
|
150
151
|
return checksResult(status, {
|
|
151
152
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
152
153
|
resource: { kind: 'pull-request', number: base.prNumber },
|
|
153
|
-
pullRequest:
|
|
154
|
+
pullRequest: base.pullRequest, checks: classified,
|
|
154
155
|
error: status === 'no-op' ? null : {
|
|
155
156
|
code: classified.state === 'pending' ? 'REQUIRED_CHECKS_PENDING' : `REQUIRED_CHECKS_${classified.state.toUpperCase()}`,
|
|
156
157
|
message: `Required checks are ${classified.state}`,
|
|
@@ -197,9 +198,9 @@ function resolvePlatformCheckRun(taskRef: string, options: SharedOptions & { che
|
|
|
197
198
|
const direct = options.detailsUrl ? parseRunJobIdentity(options.detailsUrl) : null;
|
|
198
199
|
if (direct) {
|
|
199
200
|
const run = base.client.json<{ id?: number; head_sha?: string }>(['api', `repos/${base.context.platform.repository}/actions/runs/${direct.runId}`], { cwd: base.resolved.repoRoot });
|
|
200
|
-
if (!run.ok) return checksResult(run.error.retryable ? 'blocked' : 'failed', { platform: base.context.platform, capabilities: base.context.capabilities, pullRequest:
|
|
201
|
+
if (!run.ok) return checksResult(run.error.retryable ? 'blocked' : 'failed', { platform: base.context.platform, capabilities: base.context.capabilities, pullRequest: base.pullRequest, error: run.error });
|
|
201
202
|
if (run.value.head_sha !== base.pullRequest.head.sha) return checksResult('failed', { error: { code: 'CHECK_RUN_HEAD_MISMATCH', message: 'Resolved run does not belong to the PR head SHA', retryable: false } });
|
|
202
|
-
return checksResult('no-op', { platform: base.context.platform, capabilities: base.context.capabilities, resource: { kind: 'pull-request', number: base.prNumber }, pullRequest:
|
|
203
|
+
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 });
|
|
203
204
|
}
|
|
204
205
|
const listed = base.client.json<{ check_runs?: Array<{ name?: string; details_url?: string }> }>(['api', `repos/${base.context.platform.repository}/commits/${base.pullRequest.head.sha}/check-runs`], { cwd: base.resolved.repoRoot });
|
|
205
206
|
if (!listed.ok) return checksResult(listed.error.retryable ? 'blocked' : 'failed', { error: listed.error });
|
|
@@ -211,7 +212,7 @@ function resolvePlatformCheckRun(taskRef: string, options: SharedOptions & { che
|
|
|
211
212
|
const resolution = resolveRunCandidate(candidates, base.pullRequest.head.sha, options.checkName);
|
|
212
213
|
return checksResult(resolution.status === 'resolved' ? 'no-op' : 'failed', {
|
|
213
214
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
214
|
-
resource: { kind: 'pull-request', number: base.prNumber }, pullRequest:
|
|
215
|
+
resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: base.pullRequest,
|
|
215
216
|
resolution,
|
|
216
217
|
error: resolution.status === 'resolved' ? null : { code: resolution.status === 'ambiguous' ? 'CHECK_RUN_AMBIGUOUS' : 'CHECK_RUN_NOT_FOUND', message: `Check run is ${resolution.status}`, retryable: false }
|
|
217
218
|
});
|
|
@@ -228,7 +229,7 @@ function fetchPlatformCheckLogs(taskRef: string, options: SharedOptions & { run:
|
|
|
228
229
|
if (!fetched.value) return checksResult('failed', { error: { code: 'CHECK_LOGS_MISSING', message: 'No failed logs are available', retryable: false } });
|
|
229
230
|
return checksResult('no-op', {
|
|
230
231
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
231
|
-
resource: { kind: 'pull-request', number: base.prNumber }, pullRequest:
|
|
232
|
+
resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: base.pullRequest,
|
|
232
233
|
logs: { runId: options.run, ...(options.job ? { jobId: options.job } : {}), text: fetched.value }, error: null
|
|
233
234
|
});
|
|
234
235
|
}
|
|
@@ -22,7 +22,9 @@ type PullRequestSnapshot = {
|
|
|
22
22
|
body: string;
|
|
23
23
|
draft: boolean;
|
|
24
24
|
head: { repository: string; ref: string; sha: string };
|
|
25
|
-
base: { repository: string; ref: string };
|
|
25
|
+
base: { repository: string; ref: string; sha: string };
|
|
26
|
+
mergedAt: string | null;
|
|
27
|
+
mergeCommitSha: string | null;
|
|
26
28
|
labels: string[];
|
|
27
29
|
assignees: string[];
|
|
28
30
|
milestone: string | null;
|
|
@@ -55,7 +57,9 @@ type RemotePullRequest = {
|
|
|
55
57
|
draft?: boolean;
|
|
56
58
|
pull_request?: unknown;
|
|
57
59
|
head?: { ref?: string; sha?: string; repo?: { full_name?: string } | null };
|
|
58
|
-
base?: { ref?: string; repo?: { full_name?: string } | null };
|
|
60
|
+
base?: { ref?: string; sha?: string; repo?: { full_name?: string } | null };
|
|
61
|
+
merged_at?: string | null;
|
|
62
|
+
merge_commit_sha?: string | null;
|
|
59
63
|
labels?: Array<string | { name?: string }>;
|
|
60
64
|
assignees?: Array<{ login?: string }>;
|
|
61
65
|
milestone?: { title?: string } | null;
|
|
@@ -92,7 +96,9 @@ function normalizePullRequest(remote: RemotePullRequest, repository: string): Pu
|
|
|
92
96
|
body: remote.body || '',
|
|
93
97
|
draft: Boolean(remote.draft),
|
|
94
98
|
head: { repository: headRepository, ref: remote.head.ref, sha: remote.head.sha },
|
|
95
|
-
base: { repository: baseRepository, ref: remote.base.ref },
|
|
99
|
+
base: { repository: baseRepository, ref: remote.base.ref, sha: remote.base.sha || '' },
|
|
100
|
+
mergedAt: remote.merged_at || null,
|
|
101
|
+
mergeCommitSha: remote.merge_commit_sha || null,
|
|
96
102
|
labels: (remote.labels || []).map((label) => typeof label === 'string' ? label : label.name || '').filter(Boolean).sort(),
|
|
97
103
|
assignees: (remote.assignees || []).map((assignee) => assignee.login || '').filter(Boolean).sort(),
|
|
98
104
|
milestone: remote.milestone?.title || null
|