Skip to content

Commit a0804cd

Browse files
authored
chore: implement pkg-pr-new (#592)
* chore: try pkg-pr-new * change path * fix: publish only 1 plugin * release two packages * fix: update comment * fix: add details element to comment * fix: identify changed packages * fix: publish with changesets * fix: remove changeset * update no changes message * fix: remove issue write permission * chore: add section in contributing readme to explain usage of pkg-pr-new
1 parent fe91c4d commit a0804cd

2 files changed

Lines changed: 255 additions & 0 deletions

File tree

.github/workflows/pkg-pr-new.yml

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
name: Publish
2+
on:
3+
pull_request:
4+
types: [opened, synchronize, labeled, reopened]
5+
6+
concurrency:
7+
group: pkg-pr-new-${{ github.event.pull_request.number }}
8+
cancel-in-progress: true
9+
10+
permissions:
11+
contents: read
12+
pull-requests: write
13+
14+
jobs:
15+
publish:
16+
if: >
17+
github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'trigger: preview')
18+
19+
runs-on: ubuntu-latest
20+
env:
21+
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
22+
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
23+
steps:
24+
- name: Checkout code
25+
uses: actions/checkout@v6
26+
with:
27+
fetch-depth: 0
28+
- uses: ./.github/actions/setup
29+
30+
- name: Build
31+
run: pnpm turbo run build --filter='!./dev/*'
32+
33+
- name: Detect changed plugin packages from changesets
34+
id: changed-packages
35+
run: |
36+
mapfile -t changeset_files < <(
37+
git diff --name-only "origin/${{ github.base_ref }}"...HEAD -- '.changeset/*.md'
38+
)
39+
40+
filtered_changeset_files=()
41+
for file in "${changeset_files[@]}"; do
42+
if [[ "$file" == ".changeset/README.md" ]]; then
43+
continue
44+
fi
45+
if [[ "$file" == .changeset/*.md ]]; then
46+
filtered_changeset_files+=("$file")
47+
fi
48+
done
49+
changeset_files=("${filtered_changeset_files[@]}")
50+
51+
if [ ${#changeset_files[@]} -eq 0 ]; then
52+
echo "has_changed_packages=false" >> "$GITHUB_OUTPUT"
53+
echo "package_paths=" >> "$GITHUB_OUTPUT"
54+
exit 0
55+
fi
56+
57+
parser_script="$(mktemp)"
58+
cat > "$parser_script" <<'NODE'
59+
const fs = require("node:fs")
60+
const files = process.argv.slice(2)
61+
const paths = new Set()
62+
63+
for (const file of files) {
64+
const content = fs.readFileSync(file, "utf8")
65+
const match = content.match(/^---\n([\s\S]*?)\n---/)
66+
if (!match) continue
67+
68+
for (const line of match[1].split("\n")) {
69+
const packageMatch = line.match(/^\s*['"]([^'"]+)['"]\s*:\s*(major|minor|patch)\s*$/)
70+
if (!packageMatch) continue
71+
72+
const packageName = packageMatch[1]
73+
let packagePath = null
74+
75+
if (packageName.startsWith("@sanity/")) {
76+
packagePath = `./plugins/@sanity/${packageName.slice("@sanity/".length)}`
77+
} else if (packageName.startsWith("sanity-plugin-")) {
78+
packagePath = `./plugins/${packageName}`
79+
}
80+
81+
if (packagePath && fs.existsSync(packagePath)) {
82+
paths.add(packagePath)
83+
}
84+
}
85+
}
86+
87+
for (const path of [...paths].sort()) {
88+
console.log(path)
89+
}
90+
NODE
91+
92+
mapfile -t packages < <(node "$parser_script" "${changeset_files[@]}")
93+
rm -f "$parser_script"
94+
95+
if [ ${#packages[@]} -eq 0 ]; then
96+
echo "has_changed_packages=false" >> "$GITHUB_OUTPUT"
97+
echo "package_paths=" >> "$GITHUB_OUTPUT"
98+
exit 0
99+
fi
100+
101+
echo "has_changed_packages=true" >> "$GITHUB_OUTPUT"
102+
echo "package_paths=${packages[*]}" >> "$GITHUB_OUTPUT"
103+
104+
- name: Publish preview packages
105+
if: steps.changed-packages.outputs.has_changed_packages == 'true'
106+
run: pnpm dlx pkg-pr-new publish ${{ steps.changed-packages.outputs.package_paths }} --pnpm --no-template --comment=off --json output.json
107+
108+
- name: Post or update comment
109+
uses: actions/github-script@v7
110+
env:
111+
HAS_CHANGED_PACKAGES: ${{ steps.changed-packages.outputs.has_changed_packages }}
112+
with:
113+
github-token: ${{ secrets.GITHUB_TOKEN }}
114+
script: |
115+
const fs = require('node:fs')
116+
117+
const hasChangedPackages = process.env.HAS_CHANGED_PACKAGES === 'true'
118+
const output = hasChangedPackages
119+
? JSON.parse(fs.readFileSync('output.json', 'utf8'))
120+
: {packages: []}
121+
122+
const PACKAGE_MANAGERS = [
123+
{
124+
name: 'pnpm',
125+
installCommand: 'pnpm install',
126+
logoUrl: 'https://avatars.githubusercontent.com/u/21320719?s=200&v=4',
127+
},
128+
{
129+
name: 'npm',
130+
installCommand: 'npm install',
131+
logoUrl: 'https://avatars.githubusercontent.com/u/6078720?s=200&v=4',
132+
},
133+
]
134+
135+
const BOT_COMMENT_IDENTIFIER = '<!-- pkg.pr.new -->'
136+
const BACKTICKS = '```'
137+
138+
function renderPackages(pkgManager, pkgs) {
139+
return pkgs
140+
.map(
141+
(pkg) =>
142+
`##### :package: \`${pkg.name}\`
143+
${BACKTICKS}sh
144+
${pkgManager.installCommand} ${pkg.url}
145+
${BACKTICKS}
146+
`,
147+
)
148+
.join('\n')
149+
}
150+
151+
function renderInstallInstructions() {
152+
return PACKAGE_MANAGERS.map(
153+
(pkgManager) =>
154+
`<details${pkgManager.name === 'pnpm' ? ' open' : ''}>
155+
<summary><img height="16" align="center" alt="${pkgManager.name} logo" src="${pkgManager.logoUrl}" /> <b>Using ${pkgManager.name}</b></summary>
156+
157+
${renderPackages(pkgManager, output.packages)}
158+
159+
</details>
160+
`,
161+
).join('\n')
162+
}
163+
164+
const sha = context.payload.pull_request?.head?.sha ?? context.sha
165+
166+
const body = hasChangedPackages
167+
? `## Preview this PR with [pkg.pr.new](https://pkg.pr.new)
168+
169+
${renderInstallInstructions()}
170+
171+
[View Commit](${`https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${sha}`}) (${sha})
172+
173+
${BOT_COMMENT_IDENTIFIER}
174+
`
175+
: `## Preview this PR with [pkg.pr.new](https://pkg.pr.new)
176+
177+
No packages were published, no changesets were found.
178+
179+
[View Commit](${`https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${sha}`}) (${sha})
180+
181+
${BOT_COMMENT_IDENTIFIER}
182+
`
183+
184+
async function findBotComment(issueNumber) {
185+
if (!issueNumber) return null
186+
const comments = await github.rest.issues.listComments({
187+
owner: context.repo.owner,
188+
repo: context.repo.repo,
189+
issue_number: issueNumber,
190+
})
191+
return comments.data.find((comment) => comment.body.includes(BOT_COMMENT_IDENTIFIER))
192+
}
193+
194+
async function createOrUpdateComment(issueNumber) {
195+
if (!issueNumber) {
196+
console.log('No issue number provided. Cannot post or update comment.')
197+
return
198+
}
199+
200+
const existingComment = await findBotComment(issueNumber)
201+
if (existingComment) {
202+
await github.rest.issues.updateComment({
203+
owner: context.repo.owner,
204+
repo: context.repo.repo,
205+
comment_id: existingComment.id,
206+
body,
207+
})
208+
} else {
209+
await github.rest.issues.createComment({
210+
issue_number: issueNumber,
211+
owner: context.repo.owner,
212+
repo: context.repo.repo,
213+
body,
214+
})
215+
}
216+
}
217+
218+
if (context.eventName === 'pull_request') {
219+
await createOrUpdateComment(context.issue.number)
220+
} else {
221+
throw new Error('This job can only run for pull requests')
222+
}

CONTRIBUTING.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,39 @@ Commit the changeset file with your PR.
278278

279279
This monorepo uses [Changesets](https://github.com/changesets/changesets) for version management and publishing.
280280

281+
### Preview Packages in a PR (`pkg-pr-new`)
282+
283+
You can publish preview versions of changed plugin packages directly from a PR using [`pkg.pr.new`](https://pkg.pr.new).
284+
285+
#### How it is triggered
286+
287+
The preview workflow runs on PR events **only when the PR has the `trigger: preview` label**.
288+
289+
1. Open your PR
290+
2. Add the `trigger: preview` label
291+
3. Wait for the `Publish` workflow (`.github/workflows/pkg-pr-new.yml`) to finish
292+
293+
#### What gets preview-published
294+
295+
The workflow detects changed packages from `.changeset/*.md` files in your PR diff and publishes only matching plugin packages.
296+
297+
- It ignores `.changeset/README.md`
298+
- It supports package names that map to:
299+
- `@sanity/*` -> `plugins/@sanity/*`
300+
- `sanity-plugin-*` -> `plugins/sanity-plugin-*`
301+
302+
If no valid changesets are found, no packages are published.
303+
304+
#### How to use the preview
305+
306+
After the workflow runs, it posts (or updates) a PR comment titled **"Preview this PR with pkg.pr.new"** that includes install commands for each published package (for both `pnpm` and `npm`).
307+
308+
Use the provided command in another project to test the preview package version, for example:
309+
310+
```bash
311+
pnpm install <pkg.pr.new url from the PR comment>
312+
```
313+
281314
### Creating a Changeset
282315

283316
When you make changes that should be released:

0 commit comments

Comments
 (0)