fix(actions): isolate apply-optimize from issue comments
This commit is contained in:
29
.github/workflows/skill-apply-optimize-run.yml
vendored
Normal file
29
.github/workflows/skill-apply-optimize-run.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Apply Skill Optimization Run
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: Pull request number to apply the latest Tessl optimization to
|
||||
required: true
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: skill-apply-optimize-${{ inputs.pr_number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
apply:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Apply latest Tessl optimization
|
||||
run: node tools/scripts/apply_skill_optimization.cjs
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ inputs.pr_number }}
|
||||
83
.github/workflows/skill-apply-optimize.yml
vendored
83
.github/workflows/skill-apply-optimize.yml
vendored
@@ -1,21 +1,86 @@
|
||||
name: Apply Skill Optimization
|
||||
name: Queue Skill Optimization Apply
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
apply:
|
||||
queue:
|
||||
if: >-
|
||||
github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '/apply-optimize')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
actions: write
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
ref: ${{ github.event.issue.pull_request.head.ref }}
|
||||
- uses: tesslio/skill-review-and-optimize@d81583861aaf29d1da7f10e6539efef4e27b0dd5
|
||||
with:
|
||||
mode: 'apply'
|
||||
github-token: ${{ github.token }}
|
||||
script: |
|
||||
const trusted = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']);
|
||||
const prNumber = context.payload.issue?.number;
|
||||
const association = context.payload.comment?.author_association ?? 'NONE';
|
||||
|
||||
if (!prNumber) {
|
||||
core.setFailed('No pull request number found in issue_comment payload.');
|
||||
return;
|
||||
}
|
||||
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber,
|
||||
});
|
||||
|
||||
if (!trusted.has(association)) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body:
|
||||
'⚠️ `/apply-optimize` can only be queued by repository maintainers ' +
|
||||
'(OWNER, MEMBER, or COLLABORATOR).',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (pr.head.repo.full_name !== `${context.repo.owner}/${context.repo.repo}`) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body:
|
||||
'⚠️ `/apply-optimize` only auto-applies for pull requests whose head branch ' +
|
||||
'lives in this repository. Fork PRs must apply the suggested changes manually.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'skill-apply-optimize-run.yml',
|
||||
ref: context.payload.repository.default_branch,
|
||||
inputs: {
|
||||
pr_number: String(prNumber),
|
||||
},
|
||||
});
|
||||
|
||||
await github.rest.reactions.createForIssueComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: context.payload.comment.id,
|
||||
content: 'eyes',
|
||||
});
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body:
|
||||
'👀 Queued the trusted `/apply-optimize` workflow for this PR. ' +
|
||||
'It will fetch the repository branch from a separate workflow and apply the ' +
|
||||
'latest Tessl optimization suggestion if one is available.',
|
||||
});
|
||||
|
||||
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;
|
||||
});
|
||||
@@ -25,6 +25,7 @@
|
||||
- Merged PR `#395` via GitHub squash merge after maintainer refresh of forked workflow approvals and PR body normalization; this added the new `snowflake-development` skill.
|
||||
- Merged PR `#394` via GitHub squash merge after converting the contributor branch back to source-only, normalizing the PR checklist body, and shortening an oversized `wordpress-penetration-testing` description so CI passed.
|
||||
- Patched `skills/snowflake-development/SKILL.md` on `main` with a `## When to Use` section so the repository stayed within the frozen validation warning budget after the PR merge batch.
|
||||
- Reworked `/apply-optimize` automation to address GitHub code scanning alert `#36`: the public `issue_comment` trigger now only queues a trusted workflow, while the privileged branch checkout/apply logic runs in a separate `workflow_dispatch` path limited to same-repository branches.
|
||||
- Ran the required direct-`main` maintainer sync flow after touching `skills/`:
|
||||
- `npm run chain`
|
||||
- `npm run check:warning-budget`
|
||||
|
||||
Reference in New Issue
Block a user