From 0afb519bb32ca900022550abe263aa7dc996e161 Mon Sep 17 00:00:00 2001 From: sickn33 Date: Wed, 25 Mar 2026 12:05:50 +0100 Subject: [PATCH] fix(actions): isolate apply-optimize from issue comments --- .../workflows/skill-apply-optimize-run.yml | 29 +++ .github/workflows/skill-apply-optimize.yml | 83 ++++++- tools/scripts/apply_skill_optimization.cjs | 211 ++++++++++++++++++ walkthrough.md | 1 + 4 files changed, 315 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/skill-apply-optimize-run.yml create mode 100644 tools/scripts/apply_skill_optimization.cjs diff --git a/.github/workflows/skill-apply-optimize-run.yml b/.github/workflows/skill-apply-optimize-run.yml new file mode 100644 index 00000000..464e7026 --- /dev/null +++ b/.github/workflows/skill-apply-optimize-run.yml @@ -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 }} diff --git a/.github/workflows/skill-apply-optimize.yml b/.github/workflows/skill-apply-optimize.yml index bea270a4..a158ba3f 100644 --- a/.github/workflows/skill-apply-optimize.yml +++ b/.github/workflows/skill-apply-optimize.yml @@ -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.', + }); diff --git a/tools/scripts/apply_skill_optimization.cjs b/tools/scripts/apply_skill_optimization.cjs new file mode 100644 index 00000000..af8374e8 --- /dev/null +++ b/tools/scripts/apply_skill_optimization.cjs @@ -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; +}); diff --git a/walkthrough.md b/walkthrough.md index 31dcdf8e..6617e071 100644 --- a/walkthrough.md +++ b/walkthrough.md @@ -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`