fix(actions): isolate apply-optimize from issue comments
This commit is contained in:
211
tools/scripts/apply_skill_optimization.cjs
Normal file
211
tools/scripts/apply_skill_optimization.cjs
Normal file
@@ -0,0 +1,211 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
|
||||
const REVIEW_HEADING = '## Tessl Skill Review';
|
||||
|
||||
function getRequiredEnv(name) {
|
||||
const value = process.env[name];
|
||||
if (!value) {
|
||||
throw new Error(`${name} is required`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async function githubRequest(pathname, options = {}) {
|
||||
const token = getRequiredEnv('GITHUB_TOKEN');
|
||||
const response = await fetch(`https://api.github.com${pathname}`, {
|
||||
...options,
|
||||
headers: {
|
||||
Accept: 'application/vnd.github+json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
'User-Agent': 'antigravity-awesome-skills/apply-skill-optimization',
|
||||
...(options.headers || {}),
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(`GitHub API ${pathname} failed (${response.status}): ${text}`);
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function runGit(args, options = {}) {
|
||||
return execFileSync('git', args, {
|
||||
cwd: process.cwd(),
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
function extractKeyImprovements(body) {
|
||||
const section = body.match(/\*\*Key improvements:\*\*\n((?:- .+\n?)+)/);
|
||||
if (!section) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [...section[1].matchAll(/^- (.+)$/gm)].map((match) => match[1]);
|
||||
}
|
||||
|
||||
function extractOptimizedContent(body) {
|
||||
const sections = new Map();
|
||||
const normalized = body.replace(/\r\n/g, '\n');
|
||||
const regex =
|
||||
/### `([^`]+)`[\s\S]*?View full optimized SKILL\.md[\s\S]*?```markdown\n([\s\S]*?)\n```/g;
|
||||
|
||||
let match;
|
||||
while ((match = regex.exec(normalized)) !== null) {
|
||||
const skillPath = match[1];
|
||||
const content = match[2].replace(/` ` `/g, '```');
|
||||
sections.set(skillPath, content);
|
||||
}
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
function ensureRepoRelative(filePath) {
|
||||
const cwd = process.cwd();
|
||||
const resolved = path.resolve(cwd, filePath);
|
||||
const relative = path.relative(cwd, resolved);
|
||||
|
||||
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
||||
throw new Error(`Path traversal detected: ${filePath}`);
|
||||
}
|
||||
|
||||
if (!filePath.endsWith('SKILL.md')) {
|
||||
throw new Error(`Unexpected file path (expected SKILL.md): ${filePath}`);
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
async function findLatestReviewComment(owner, repo, prNumber) {
|
||||
let page = 1;
|
||||
let latest = null;
|
||||
|
||||
while (true) {
|
||||
const comments = await githubRequest(
|
||||
`/repos/${owner}/${repo}/issues/${prNumber}/comments?per_page=100&page=${page}`,
|
||||
);
|
||||
|
||||
for (const comment of comments) {
|
||||
if (comment.body && comment.body.includes(REVIEW_HEADING)) {
|
||||
latest = comment;
|
||||
}
|
||||
}
|
||||
|
||||
if (comments.length < 100) {
|
||||
return latest;
|
||||
}
|
||||
|
||||
page += 1;
|
||||
}
|
||||
}
|
||||
|
||||
async function postComment(owner, repo, prNumber, body) {
|
||||
await githubRequest(`/repos/${owner}/${repo}/issues/${prNumber}/comments`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ body }),
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const repoSlug = getRequiredEnv('GITHUB_REPOSITORY');
|
||||
const [owner, repo] = repoSlug.split('/');
|
||||
const prNumber = process.env.PR_NUMBER || JSON.parse(
|
||||
fs.readFileSync(getRequiredEnv('GITHUB_EVENT_PATH'), 'utf8'),
|
||||
).inputs?.pr_number;
|
||||
|
||||
if (!prNumber) {
|
||||
throw new Error('PR_NUMBER or workflow_dispatch input pr_number is required');
|
||||
}
|
||||
|
||||
const pr = await githubRequest(`/repos/${owner}/${repo}/pulls/${prNumber}`);
|
||||
|
||||
if (pr.head.repo.full_name !== repoSlug) {
|
||||
throw new Error(
|
||||
'Auto-apply is only supported for PR branches that live in the base repository.',
|
||||
);
|
||||
}
|
||||
|
||||
const reviewComment = await findLatestReviewComment(owner, repo, prNumber);
|
||||
if (!reviewComment || !reviewComment.body) {
|
||||
throw new Error('No Tessl skill review comment found on this PR. Run the review first.');
|
||||
}
|
||||
|
||||
const optimizedFiles = extractOptimizedContent(reviewComment.body);
|
||||
if (optimizedFiles.size === 0) {
|
||||
throw new Error('No optimized SKILL.md content found in the latest Tessl review comment.');
|
||||
}
|
||||
|
||||
const branch = pr.head.ref;
|
||||
console.log(`Applying optimization to PR #${prNumber} on ${branch}`);
|
||||
|
||||
runGit(['fetch', 'origin', branch]);
|
||||
runGit(['checkout', '-B', branch, `origin/${branch}`]);
|
||||
|
||||
for (const [filePath, content] of optimizedFiles.entries()) {
|
||||
const resolved = ensureRepoRelative(filePath);
|
||||
fs.writeFileSync(resolved, content);
|
||||
console.log(`Updated ${filePath}`);
|
||||
}
|
||||
|
||||
runGit(['config', 'user.name', 'tessl-skill-review[bot]']);
|
||||
runGit(['config', 'user.email', 'skill-review[bot]@users.noreply.github.com']);
|
||||
runGit(['add', ...optimizedFiles.keys()]);
|
||||
|
||||
let committed = true;
|
||||
try {
|
||||
runGit(['commit', '-m', 'Apply optimized SKILL.md from Tessl review']);
|
||||
} catch (error) {
|
||||
const output = `${error.stdout || ''}\n${error.stderr || ''}`;
|
||||
if (output.includes('nothing to commit')) {
|
||||
committed = false;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (!committed) {
|
||||
await postComment(
|
||||
owner,
|
||||
repo,
|
||||
prNumber,
|
||||
'⚠️ No changes to apply. The PR branch already matches the latest Tessl optimization.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
runGit(['push', 'origin', `HEAD:${branch}`]);
|
||||
|
||||
const shortSha = runGit(['rev-parse', '--short', 'HEAD']).trim();
|
||||
const updatedFiles = [...optimizedFiles.keys()].map((item) => `\`${item}\``).join(', ');
|
||||
const improvements = extractKeyImprovements(reviewComment.body);
|
||||
|
||||
let body = `✅ Applied optimized ${updatedFiles} (${shortSha}).`;
|
||||
if (improvements.length > 0) {
|
||||
body += '\n\n**What changed:**';
|
||||
for (const item of improvements.slice(0, 3)) {
|
||||
body += `\n- ${item}`;
|
||||
}
|
||||
}
|
||||
|
||||
await postComment(owner, repo, prNumber, body);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.stack : String(error));
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Reference in New Issue
Block a user