From 13479976bdb2f0737135e35183809aa67d966d30 Mon Sep 17 00:00:00 2001 From: kongenpei Date: Wed, 1 Apr 2026 11:41:42 +0800 Subject: [PATCH 1/5] ci: make pkg.pr.new comment flow fork-safe --- .github/workflows/pkg-pr-new-comment.yml | 102 +++++++++++++++++++++++ .github/workflows/pkg-pr-new.yml | 101 ++++++++-------------- 2 files changed, 136 insertions(+), 67 deletions(-) create mode 100644 .github/workflows/pkg-pr-new-comment.yml diff --git a/.github/workflows/pkg-pr-new-comment.yml b/.github/workflows/pkg-pr-new-comment.yml new file mode 100644 index 00000000..33211a4e --- /dev/null +++ b/.github/workflows/pkg-pr-new-comment.yml @@ -0,0 +1,102 @@ +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: Download comment payload + 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 + 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 issueNumber = payload?.pr; + const sourceRepo = payload?.sourceRepo; + const sourceBranch = payload?.sourceBranch; + + if (!url || !url.startsWith("https://pkg.pr.new/")) { + throw new Error(`Invalid package URL in payload: ${url}`); + } + if (!Number.isInteger(issueNumber)) { + throw new Error(`Invalid PR number in payload: ${issueNumber}`); + } + + const hasSkillSource = Boolean(sourceRepo && 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 From d8261c3dfec978454c11a5a78f9b0e4d44415922 Mon Sep 17 00:00:00 2001 From: kongenpei Date: Wed, 1 Apr 2026 11:48:11 +0800 Subject: [PATCH 2/5] ci: harden trusted comment workflow inputs --- .github/workflows/pkg-pr-new-comment.yml | 36 ++++++++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pkg-pr-new-comment.yml b/.github/workflows/pkg-pr-new-comment.yml index 33211a4e..b9de6e8a 100644 --- a/.github/workflows/pkg-pr-new-comment.yml +++ b/.github/workflows/pkg-pr-new-comment.yml @@ -9,7 +9,6 @@ permissions: actions: read contents: read issues: write - pull-requests: write jobs: comment: @@ -32,18 +31,43 @@ jobs: const fs = require("fs"); const payload = JSON.parse(fs.readFileSync("pkg-pr-new-comment-payload.json", "utf8")); const url = payload?.url; - const issueNumber = payload?.pr; + const payloadPr = payload?.pr; const sourceRepo = payload?.sourceRepo; const sourceBranch = payload?.sourceBranch; + const issueNumber = context.payload.workflow_run?.pull_requests?.[0]?.number; - if (!url || !url.startsWith("https://pkg.pr.new/")) { + if (!Number.isInteger(issueNumber)) { + throw new Error(`Invalid PR number in workflow_run payload: ${issueNumber}`); + } + if (!Number.isInteger(payloadPr)) { + throw new Error(`Invalid PR number in artifact payload: ${payloadPr}`); + } + if (payloadPr !== issueNumber) { + throw new Error( + `PR number mismatch between workflow_run (${issueNumber}) and artifact payload (${payloadPr})`, + ); + } + + if (typeof url !== "string" || url.trim() !== url || /[\u0000-\u001F\u007F]/.test(url)) { throw new Error(`Invalid package URL in payload: ${url}`); } - if (!Number.isInteger(issueNumber)) { - throw new Error(`Invalid PR number in payload: ${issueNumber}`); + 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 hasSkillSource = Boolean(sourceRepo && sourceBranch); + 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 ? [ "", From 7678b5b0eb884944150242ac99474fa5b1a734db Mon Sep 17 00:00:00 2001 From: kongenpei Date: Wed, 1 Apr 2026 11:53:56 +0800 Subject: [PATCH 3/5] ci: skip comment steps when payload artifact is missing --- .github/workflows/pkg-pr-new-comment.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/pkg-pr-new-comment.yml b/.github/workflows/pkg-pr-new-comment.yml index b9de6e8a..ec750b3e 100644 --- a/.github/workflows/pkg-pr-new-comment.yml +++ b/.github/workflows/pkg-pr-new-comment.yml @@ -16,7 +16,28 @@ jobs: 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 @@ -25,6 +46,7 @@ jobs: github-token: ${{ github.token }} - name: Comment install command + if: steps.payload.outputs.found == 'true' uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: script: | From 5e3e156fa36ccdd0f6fbf33e8746e9f978319bcc Mon Sep 17 00:00:00 2001 From: kongenpei Date: Wed, 1 Apr 2026 12:01:20 +0800 Subject: [PATCH 4/5] ci: use artifact PR number when workflow_run pull_requests is empty --- .github/workflows/pkg-pr-new-comment.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pkg-pr-new-comment.yml b/.github/workflows/pkg-pr-new-comment.yml index ec750b3e..79a25541 100644 --- a/.github/workflows/pkg-pr-new-comment.yml +++ b/.github/workflows/pkg-pr-new-comment.yml @@ -56,17 +56,17 @@ jobs: const payloadPr = payload?.pr; const sourceRepo = payload?.sourceRepo; const sourceBranch = payload?.sourceBranch; - const issueNumber = context.payload.workflow_run?.pull_requests?.[0]?.number; - - if (!Number.isInteger(issueNumber)) { - throw new Error(`Invalid PR number in workflow_run payload: ${issueNumber}`); - } if (!Number.isInteger(payloadPr)) { throw new Error(`Invalid PR number in artifact payload: ${payloadPr}`); } - if (payloadPr !== issueNumber) { + 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 (${issueNumber}) and artifact payload (${payloadPr})`, + `PR number mismatch between workflow_run (${runPrNumber}) and artifact payload (${issueNumber})`, ); } From 8c1969ca4afa677e57f13411aa759b61b9ce13bc Mon Sep 17 00:00:00 2001 From: kongenpei Date: Wed, 1 Apr 2026 13:55:00 +0800 Subject: [PATCH 5/5] ci: allow PR comment workflow to write pull requests --- .github/workflows/pkg-pr-new-comment.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pkg-pr-new-comment.yml b/.github/workflows/pkg-pr-new-comment.yml index 79a25541..56725fc6 100644 --- a/.github/workflows/pkg-pr-new-comment.yml +++ b/.github/workflows/pkg-pr-new-comment.yml @@ -9,6 +9,7 @@ permissions: actions: read contents: read issues: write + pull-requests: write jobs: comment: