diff --git a/.github/workflows/pkg-pr-new-comment.yml b/.github/workflows/pkg-pr-new-comment.yml new file mode 100644 index 00000000..79a25541 --- /dev/null +++ b/.github/workflows/pkg-pr-new-comment.yml @@ -0,0 +1,148 @@ +name: PR Preview Package Comment + +on: + workflow_run: + workflows: ["PR Preview Package"] + types: [completed] + +permissions: + actions: read + contents: read + issues: 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 new file mode 100644 index 00000000..a6582fa5 --- /dev/null +++ b/.github/workflows/pkg-pr-new.yml @@ -0,0 +1,71 @@ +name: PR Preview Package + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: [main] + +permissions: + contents: read + +jobs: + publish: + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + with: + go-version-file: go.mod + + - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4 + with: + node-version: lts/* + + - name: Build preview package + run: ./scripts/build-pkg-pr-new.sh + + - name: Publish to pkg.pr.new + run: npx pkg-pr-new publish --no-compact --json output.json --comment=off ./.pkg-pr-new + + - 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 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 pr = Number(process.env.PR_NUMBER); + if (!Number.isInteger(pr) || pr <= 0) { + throw new Error(`Invalid PR_NUMBER: ${process.env.PR_NUMBER}`); + } + + const payload = { + pr, + url, + sourceRepo: process.env.SOURCE_REPO || "", + sourceBranch: process.env.SOURCE_BRANCH || "", + }; + + fs.writeFileSync( + "pkg-pr-new-comment-payload.json", + JSON.stringify(payload), + ); + NODE + + - name: Upload comment payload + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: pkg-pr-new-comment-payload + path: pkg-pr-new-comment-payload.json diff --git a/scripts/build-pkg-pr-new.sh b/scripts/build-pkg-pr-new.sh new file mode 100755 index 00000000..caa65231 --- /dev/null +++ b/scripts/build-pkg-pr-new.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +OUT_DIR="$ROOT_DIR/.pkg-pr-new" + +cd "$ROOT_DIR" + +python3 scripts/fetch_meta.py + +rm -rf "$OUT_DIR" +mkdir -p "$OUT_DIR/bin" "$OUT_DIR/scripts" + +VERSION="$(node -p "require('./package.json').version")" +DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" +SHA="$(git rev-parse --short HEAD)" +LDFLAGS="-s -w -X github.com/larksuite/cli/internal/build.Version=${VERSION}-${SHA} -X github.com/larksuite/cli/internal/build.Date=${DATE}" + +build_target() { + local goos="$1" + local goarch="$2" + local ext="" + if [[ "$goos" == "windows" ]]; then + ext=".exe" + fi + + local output="$OUT_DIR/bin/lark-cli-${goos}-${goarch}${ext}" + echo "Building ${goos}/${goarch} -> ${output}" + CGO_ENABLED=0 GOOS="$goos" GOARCH="$goarch" go build -trimpath -ldflags "$LDFLAGS" -o "$output" ./main.go +} + +build_target darwin arm64 + +cat > "$OUT_DIR/scripts/run.js" <<'RUNJS' +#!/usr/bin/env node +const path = require("path"); +const { execFileSync } = require("child_process"); + +const isWindows = process.platform === "win32"; + +const platformMap = { + darwin: "darwin", + linux: "linux", + win32: "windows", +}; + +const archMap = { + x64: "amd64", + arm64: "arm64", +}; + +const platform = platformMap[process.platform]; +const arch = archMap[process.arch]; + +if (!platform || !arch) { + console.error(`Unsupported platform: ${process.platform}-${process.arch}`); + process.exit(1); +} + +const ext = isWindows ? ".exe" : ""; +const binary = path.join(__dirname, "..", "bin", `lark-cli-${platform}-${arch}${ext}`); + +try { + execFileSync(binary, process.argv.slice(2), { stdio: "inherit" }); +} catch (err) { + process.exit(err.status || 1); +} +RUNJS + +chmod +x "$OUT_DIR/scripts/run.js" + +cat > "$OUT_DIR/package.json" <