diff --git a/.github/workflows/pkg-pr-new-comment.yml b/.github/workflows/pkg-pr-new-comment.yml new file mode 100644 index 00000000..56725fc6 --- /dev/null +++ b/.github/workflows/pkg-pr-new-comment.yml @@ -0,0 +1,149 @@ +name: PR Preview Package Comment + +on: + workflow_run: + workflows: ["PR Preview Package"] + types: [completed] + +permissions: + actions: read + contents: read + issues: write + pull-requests: write + +jobs: + comment: + if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' + runs-on: ubuntu-latest + + steps: + - name: Check comment payload artifact + id: payload + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const runId = context.payload.workflow_run?.id; + const { data } = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: runId, + per_page: 100, + }); + const found = Boolean( + data.artifacts?.some((artifact) => artifact.name === "pkg-pr-new-comment-payload") + ); + core.setOutput("found", found ? "true" : "false"); + if (!found) { + core.notice("No comment payload artifact found for this run; skipping comment."); + } + + - name: Download comment payload + if: steps.payload.outputs.found == 'true' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: pkg-pr-new-comment-payload + repository: ${{ github.repository }} + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + + - name: Comment install command + if: steps.payload.outputs.found == 'true' + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const fs = require("fs"); + const payload = JSON.parse(fs.readFileSync("pkg-pr-new-comment-payload.json", "utf8")); + const url = payload?.url; + const payloadPr = payload?.pr; + const sourceRepo = payload?.sourceRepo; + const sourceBranch = payload?.sourceBranch; + if (!Number.isInteger(payloadPr)) { + throw new Error(`Invalid PR number in artifact payload: ${payloadPr}`); + } + if (payloadPr <= 0) { + throw new Error(`Invalid PR number in artifact payload: ${payloadPr}`); + } + const issueNumber = payloadPr; + const runPrNumber = context.payload.workflow_run?.pull_requests?.[0]?.number; + if (Number.isInteger(runPrNumber) && runPrNumber !== issueNumber) { + throw new Error( + `PR number mismatch between workflow_run (${runPrNumber}) and artifact payload (${issueNumber})`, + ); + } + + if (typeof url !== "string" || url.trim() !== url || /[\u0000-\u001F\u007F]/.test(url)) { + throw new Error(`Invalid package URL in payload: ${url}`); + } + let parsedUrl; + try { + parsedUrl = new URL(url); + } catch { + throw new Error(`Invalid package URL in payload: ${url}`); + } + if (parsedUrl.protocol !== "https:" || parsedUrl.hostname !== "pkg.pr.new") { + throw new Error(`Invalid package URL in payload: ${url}`); + } + + const safeRepoPattern = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/; + const safeBranchPattern = /^[A-Za-z0-9._\/-]+$/; + const hasSkillSource = + typeof sourceRepo === "string" && + typeof sourceBranch === "string" && + safeRepoPattern.test(sourceRepo) && + safeBranchPattern.test(sourceBranch); + const skillSection = hasSkillSource + ? [ + "", + "### 🧩 Skill update", + "", + "```bash", + `npx skills add ${sourceRepo}#${sourceBranch} -y -g`, + "```", + ] + : [ + "", + "### 🧩 Skill update", + "", + "_Unavailable for this PR because source repo/branch metadata is missing._", + ]; + + const body = [ + "", + "## 🚀 PR Preview Install Guide", + "", + "### 🧰 CLI update", + "", + "```bash", + `npm i -g ${url}`, + "```", + ...skillSection, + ].join("\n"); + + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + per_page: 100, + }); + + const existing = comments.find((comment) => + comment.user?.login === "github-actions[bot]" && + typeof comment.body === "string" && + comment.body.includes("") + ); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body, + }); + } diff --git a/.github/workflows/pkg-pr-new.yml b/.github/workflows/pkg-pr-new.yml index ee07e71f..a6582fa5 100644 --- a/.github/workflows/pkg-pr-new.yml +++ b/.github/workflows/pkg-pr-new.yml @@ -7,7 +7,6 @@ on: permissions: contents: read - pull-requests: write jobs: publish: @@ -31,74 +30,42 @@ jobs: - name: Publish to pkg.pr.new run: npx pkg-pr-new publish --no-compact --json output.json --comment=off ./.pkg-pr-new - - name: Comment install command - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 - with: - script: | - const fs = require("fs"); - const output = JSON.parse(fs.readFileSync("output.json", "utf8")); - const url = output?.packages?.[0]?.url; - if (!url) { - throw new Error("No package URL found in output.json"); - } - const sourceRepo = context.payload.pull_request?.head?.repo?.full_name; - const sourceBranch = context.payload.pull_request?.head?.ref; - const hasSkillSource = Boolean(sourceRepo && sourceBranch); + - name: Build comment payload + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + SOURCE_REPO: ${{ github.event.pull_request.head.repo.full_name }} + SOURCE_BRANCH: ${{ github.event.pull_request.head.ref }} + run: | + node <<'NODE' + const fs = require("fs"); - const skillSection = hasSkillSource - ? [ - "", - "### 🧩 Skill update", - "", - "```bash", - `npx skills add ${sourceRepo}#${sourceBranch} -y -g`, - "```", - ] - : [ - "", - "### 🧩 Skill update", - "", - "_Unavailable for this PR because source repo/branch metadata is missing._", - ]; + const output = JSON.parse(fs.readFileSync("output.json", "utf8")); + const url = output?.packages?.[0]?.url; + if (!url) throw new Error("No package URL found in output.json"); + if (!url.startsWith("https://pkg.pr.new/")) { + throw new Error(`Unexpected package URL: ${url}`); + } - const body = [ - "", - "## 🚀 PR Preview Install Guide", - "", - "### 🧰 CLI update", - "", - "```bash", - `npm i -g ${url}`, - "```", - ...skillSection, - ].join("\n"); - const issueNumber = context.issue.number; + const pr = Number(process.env.PR_NUMBER); + if (!Number.isInteger(pr) || pr <= 0) { + throw new Error(`Invalid PR_NUMBER: ${process.env.PR_NUMBER}`); + } - const comments = await github.paginate(github.rest.issues.listComments, { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - per_page: 100, - }); + const payload = { + pr, + url, + sourceRepo: process.env.SOURCE_REPO || "", + sourceBranch: process.env.SOURCE_BRANCH || "", + }; - const existing = comments.find((comment) => - comment.user?.login === "github-actions[bot]" && - typeof comment.body === "string" && - comment.body.includes("") - ); + fs.writeFileSync( + "pkg-pr-new-comment-payload.json", + JSON.stringify(payload), + ); + NODE - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - body, - }); - } + - name: Upload comment payload + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: pkg-pr-new-comment-payload + path: pkg-pr-new-comment-payload.json