fix(actions): isolate apply-optimize from issue comments

This commit is contained in:
sickn33
2026-03-25 12:05:50 +01:00
parent 09f4b5ed8d
commit 0afb519bb3
4 changed files with 315 additions and 9 deletions

View 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 }}

View File

@@ -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.',
});

View 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;
});

View File

@@ -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`