@icebreakers/monorepo-templates 1.3.1 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/README.zh-CN.md +6 -0
- package/assets/.github/auto-changeset/github.mjs +90 -0
- package/assets/.github/auto-changeset/index.mjs +218 -0
- package/assets/.github/workflows/ci.yml +11 -0
- package/assets/.github/workflows/release-intent-auto.yml +40 -0
- package/assets/.github/workflows/release-intent-check.yml +57 -0
- package/assets/CONTRIBUTING.md +7 -1
- package/assets/package.json +57 -14
- package/assets/pnpm-workspace.yaml +13 -0
- package/assets/repoctl.config.ts +6 -1
- package/assets/turbo.json +13 -0
- package/package.json +4 -4
- package/templates/client/eslint.config.js +1 -1
- package/templates/client/package.json +10 -10
- package/templates/client/worker-configuration.d.ts +301 -16
- package/templates/server/package.json +3 -3
- package/templates/server/worker-configuration.d.ts +301 -16
- package/templates/vitepress/package.json +3 -3
- package/templates/vitepress/zh/learn/tools/turborepo.md +10 -10
- package/templates/vue-lib/package.json +3 -3
package/README.md
CHANGED
|
@@ -22,3 +22,9 @@ const vitepress = getTemplateDefinition('vitepress')
|
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
The source workspaces under the repository's `templates/` directory are private. This package is their supported distribution boundary.
|
|
25
|
+
|
|
26
|
+
## Project links
|
|
27
|
+
|
|
28
|
+
- Documentation: https://repoctl.icebreaker.top
|
|
29
|
+
- Repository: https://github.com/sonofmagic/repoctl/tree/main/packages/monorepo-templates
|
|
30
|
+
- Issues: https://github.com/sonofmagic/repoctl/issues
|
package/README.zh-CN.md
CHANGED
|
@@ -14,3 +14,9 @@ const vitepress = getTemplateDefinition('vitepress')
|
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
仓库 `templates/` 下的源码工作区均为私有,本包是这些模板的正式分发边界。
|
|
17
|
+
|
|
18
|
+
## 项目链接
|
|
19
|
+
|
|
20
|
+
- 文档:https://repoctl.icebreaker.top
|
|
21
|
+
- 仓库:https://github.com/sonofmagic/repoctl/tree/main/packages/monorepo-templates
|
|
22
|
+
- 问题反馈:https://github.com/sonofmagic/repoctl/issues
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
|
|
3
|
+
function repositoryParts(repository) {
|
|
4
|
+
const [owner, name] = repository.split('/')
|
|
5
|
+
if (!owner || !name) {
|
|
6
|
+
throw new Error(`Invalid GitHub repository: ${repository}`)
|
|
7
|
+
}
|
|
8
|
+
return { owner, name }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function encodePath(path) {
|
|
12
|
+
return path.split('/').map(segment => encodeURIComponent(segment)).join('/')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createGitHubClient({ token, repository, fetchImpl = fetch }) {
|
|
16
|
+
const { owner, name } = repositoryParts(repository)
|
|
17
|
+
const apiBase = `https://api.github.com/repos/${owner}/${name}`
|
|
18
|
+
async function request(path, options = {}) {
|
|
19
|
+
const response = await fetchImpl(`${apiBase}${path}`, {
|
|
20
|
+
...options,
|
|
21
|
+
headers: {
|
|
22
|
+
'accept': 'application/vnd.github+json',
|
|
23
|
+
'authorization': `Bearer ${token}`,
|
|
24
|
+
'content-type': 'application/json',
|
|
25
|
+
'x-github-api-version': '2022-11-28',
|
|
26
|
+
...(options.headers ?? {}),
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
const text = await response.text()
|
|
30
|
+
let body
|
|
31
|
+
try {
|
|
32
|
+
body = text ? JSON.parse(text) : undefined
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
body = text
|
|
36
|
+
}
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
throw new Error(`GitHub API ${response.status} ${options.method ?? 'GET'} ${path}: ${typeof body === 'string' ? body : body?.message ?? 'request failed'}`)
|
|
39
|
+
}
|
|
40
|
+
return body
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
repository,
|
|
44
|
+
getPullRequest: number => request(`/pulls/${number}`),
|
|
45
|
+
getPullRequestFiles: async (number) => {
|
|
46
|
+
const files = []
|
|
47
|
+
for (let page = 1; ; page++) {
|
|
48
|
+
const pageFiles = await request(`/pulls/${number}/files?per_page=100&page=${page}`)
|
|
49
|
+
files.push(...pageFiles)
|
|
50
|
+
if (pageFiles.length < 100) {
|
|
51
|
+
return files
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
getContent: async (path, ref) => {
|
|
56
|
+
try {
|
|
57
|
+
const content = await request(`/contents/${encodePath(path)}?ref=${encodeURIComponent(ref)}`)
|
|
58
|
+
if (Array.isArray(content) || content.encoding !== 'base64') {
|
|
59
|
+
return undefined
|
|
60
|
+
}
|
|
61
|
+
return Buffer.from(content.content.replace(/\n/g, ''), 'base64').toString('utf8')
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (String(error.message).startsWith('GitHub API 404')) {
|
|
65
|
+
return undefined
|
|
66
|
+
}
|
|
67
|
+
throw error
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
getRef: ref => request(`/git/ref/${encodePath(ref)}`),
|
|
71
|
+
getCommit: sha => request(`/git/commits/${sha}`),
|
|
72
|
+
createBlob: content => request('/git/blobs', { method: 'POST', body: JSON.stringify({ content, encoding: 'utf-8' }) }),
|
|
73
|
+
createTree: (baseTree, tree) => request('/git/trees', { method: 'POST', body: JSON.stringify({ base_tree: baseTree, tree }) }),
|
|
74
|
+
createCommit: (message, tree, parents) => request('/git/commits', { method: 'POST', body: JSON.stringify({ message, tree, parents }) }),
|
|
75
|
+
updateRef: (ref, sha) => request(`/git/refs/heads/${encodePath(ref)}`, { method: 'PATCH', body: JSON.stringify({ sha, force: false }) }),
|
|
76
|
+
createComment: (number, body) => request(`/issues/${number}/comments`, { method: 'POST', body: JSON.stringify({ body }) }),
|
|
77
|
+
listComments: async (number) => {
|
|
78
|
+
const comments = []
|
|
79
|
+
for (let page = 1; ; page++) {
|
|
80
|
+
const pageComments = await request(`/issues/${number}/comments?per_page=100&page=${page}`)
|
|
81
|
+
comments.push(...pageComments)
|
|
82
|
+
if (pageComments.length < 100) {
|
|
83
|
+
return comments
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
createBranch: async (branch, sha) => request('/git/refs', { method: 'POST', body: JSON.stringify({ ref: `refs/heads/${branch}`, sha }) }),
|
|
88
|
+
createPullRequest: input => request('/pulls', { method: 'POST', body: JSON.stringify(input) }),
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import process from 'node:process'
|
|
3
|
+
import { createGitHubClient } from './github.mjs'
|
|
4
|
+
|
|
5
|
+
const CHANGESET_MARKER = '<!-- repoctl-auto-changeset -->'
|
|
6
|
+
const PACKAGE_PATH = /^packages\/([^/]+)\//
|
|
7
|
+
const CHANGESET_PATH = /^\.changeset\/[^/]+\.md$/
|
|
8
|
+
const RELEASE_BRANCH = /^(?:pnpm-release\/|release\/pnpm-version)/
|
|
9
|
+
|
|
10
|
+
export function packageDirectories(files) {
|
|
11
|
+
return [...new Set(files
|
|
12
|
+
.map(file => file.filename ?? file)
|
|
13
|
+
.filter(filename => !/^packages\/[^/]+\/dist(?:\/|$)/.test(filename))
|
|
14
|
+
.map(filename => filename.match(PACKAGE_PATH)?.[1])
|
|
15
|
+
.filter(Boolean))]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function releaseLevel(labels = []) {
|
|
19
|
+
const names = labels.map(label => typeof label === 'string' ? label : label.name)
|
|
20
|
+
if (names.includes('release:major')) {
|
|
21
|
+
return 'major'
|
|
22
|
+
}
|
|
23
|
+
if (names.includes('release:minor')) {
|
|
24
|
+
return 'minor'
|
|
25
|
+
}
|
|
26
|
+
return 'patch'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function renderChangeset({ packages, level = 'patch', number, title }) {
|
|
30
|
+
const frontmatter = packages
|
|
31
|
+
.slice()
|
|
32
|
+
.sort((a, b) => a.localeCompare(b))
|
|
33
|
+
.map(name => `${/^[\w.-]+$/.test(name) ? name : JSON.stringify(name)}: ${level}`)
|
|
34
|
+
.join('\n')
|
|
35
|
+
const summary = title.replace(/\r?\n/g, ' ').replace(/\s+/g, ' ').trim()
|
|
36
|
+
return `---\n${frontmatter}\n---\n\n${summary} (#${number})\n`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function changesetPath(number) {
|
|
40
|
+
return `.changeset/auto-pr-${number}.md`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function isReleaseBranch(ref) {
|
|
44
|
+
return RELEASE_BRANCH.test(ref)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function hasChangeset(files) {
|
|
48
|
+
return files.some(file => CHANGESET_PATH.test(file.filename ?? file))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function hasManualChangeset(files, generatedPath) {
|
|
52
|
+
return files.some((file) => {
|
|
53
|
+
const filename = file.filename ?? file
|
|
54
|
+
return CHANGESET_PATH.test(filename) && filename !== generatedPath
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function isPublishableManifest(manifest) {
|
|
59
|
+
return Boolean(manifest && typeof manifest.name === 'string' && manifest.private !== true)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function manifestsForFiles(client, files, ref) {
|
|
63
|
+
const names = []
|
|
64
|
+
for (const directory of packageDirectories(files)) {
|
|
65
|
+
const content = await client.getContent(`packages/${directory}/package.json`, ref)
|
|
66
|
+
if (!content) {
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const manifest = JSON.parse(content)
|
|
71
|
+
if (isPublishableManifest(manifest)) {
|
|
72
|
+
names.push(manifest.name)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Ignore a malformed manifest; the normal CI checks will report it.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return [...new Set(names)]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function commitChangesets(client, branch, baseSha, entries) {
|
|
83
|
+
const commit = await client.getCommit(baseSha)
|
|
84
|
+
const tree = []
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
const blob = await client.createBlob(entry.content)
|
|
87
|
+
tree.push({ path: entry.path, mode: '100644', type: 'blob', sha: blob.sha })
|
|
88
|
+
}
|
|
89
|
+
const nextTree = await client.createTree(commit.tree.sha, tree)
|
|
90
|
+
const nextCommit = await client.createCommit('chore(release): add automated changesets', nextTree.sha, [baseSha])
|
|
91
|
+
await client.updateRef(branch, nextCommit.sha)
|
|
92
|
+
return nextCommit.sha
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function commentFork(client, number) {
|
|
96
|
+
const comments = await client.listComments(number)
|
|
97
|
+
if (comments.some(comment => comment.body?.includes(CHANGESET_MARKER))) {
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
await client.createComment(number, `${CHANGESET_MARKER}\nThis PR changes a publishable package. Please add a .changeset/*.md release intent before merging. You can run pnpm change locally.`)
|
|
101
|
+
return true
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function prepareEntry(client, pullRequest, files) {
|
|
105
|
+
const path = changesetPath(pullRequest.number)
|
|
106
|
+
if (pullRequest.draft || isReleaseBranch(pullRequest.head.ref) || hasManualChangeset(files, path)) {
|
|
107
|
+
return undefined
|
|
108
|
+
}
|
|
109
|
+
const packages = await manifestsForFiles(client, files, pullRequest.head.sha)
|
|
110
|
+
if (!packages.length) {
|
|
111
|
+
return undefined
|
|
112
|
+
}
|
|
113
|
+
const level = releaseLevel(pullRequest.labels)
|
|
114
|
+
const content = renderChangeset({ packages, level, number: pullRequest.number, title: pullRequest.title })
|
|
115
|
+
const existing = await client.getContent(path, pullRequest.head.sha)
|
|
116
|
+
if (existing && !['patch', 'minor', 'major'].some(candidate => existing === renderChangeset({ packages, level: candidate, number: pullRequest.number, title: pullRequest.title }))) {
|
|
117
|
+
return undefined
|
|
118
|
+
}
|
|
119
|
+
if (existing === content) {
|
|
120
|
+
return undefined
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
path,
|
|
124
|
+
content,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function processPullRequest(client, number) {
|
|
129
|
+
const pullRequest = await client.getPullRequest(number)
|
|
130
|
+
const files = await client.getPullRequestFiles(number)
|
|
131
|
+
const entry = await prepareEntry(client, pullRequest, files)
|
|
132
|
+
if (!entry) {
|
|
133
|
+
return { action: 'skipped', number }
|
|
134
|
+
}
|
|
135
|
+
const repository = pullRequest.base.repo.full_name
|
|
136
|
+
if (pullRequest.head.repo.full_name !== repository) {
|
|
137
|
+
await commentFork(client, number)
|
|
138
|
+
return { action: 'commented', number }
|
|
139
|
+
}
|
|
140
|
+
const ref = await client.getRef(`heads/${pullRequest.head.ref}`)
|
|
141
|
+
await commitChangesets(client, pullRequest.head.ref, ref.object.sha, [entry])
|
|
142
|
+
return { action: 'committed', number, path: entry.path }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function backfillPullRequests(client, numbers, { runId = `${Date.now()}` } = {}) {
|
|
146
|
+
const baseRef = await client.getRef('heads/main')
|
|
147
|
+
const entries = []
|
|
148
|
+
const seen = new Set()
|
|
149
|
+
const results = []
|
|
150
|
+
for (const number of numbers) {
|
|
151
|
+
const pullRequest = await client.getPullRequest(number)
|
|
152
|
+
if (!pullRequest.merged_at || pullRequest.base.ref !== 'main') {
|
|
153
|
+
results.push({ action: 'skipped', number })
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
const files = await client.getPullRequestFiles(number)
|
|
157
|
+
const path = changesetPath(number)
|
|
158
|
+
if (hasChangeset(files) || seen.has(path) || await client.getContent(path, baseRef.object.sha)) {
|
|
159
|
+
results.push({ action: 'skipped', number })
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
const packages = await manifestsForFiles(client, files, pullRequest.merge_commit_sha)
|
|
163
|
+
if (!packages.length) {
|
|
164
|
+
results.push({ action: 'skipped', number })
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
seen.add(path)
|
|
168
|
+
entries.push({
|
|
169
|
+
path,
|
|
170
|
+
content: renderChangeset({ packages, level: releaseLevel(pullRequest.labels), number, title: pullRequest.title }),
|
|
171
|
+
})
|
|
172
|
+
results.push({ action: 'planned', number, path })
|
|
173
|
+
}
|
|
174
|
+
if (!entries.length) {
|
|
175
|
+
return { results, pullRequest: undefined }
|
|
176
|
+
}
|
|
177
|
+
const branch = `automation/changeset-backfill-${runId}`
|
|
178
|
+
await client.createBranch(branch, baseRef.object.sha)
|
|
179
|
+
await commitChangesets(client, branch, baseRef.object.sha, entries)
|
|
180
|
+
const created = await client.createPullRequest({
|
|
181
|
+
base: 'main',
|
|
182
|
+
head: branch,
|
|
183
|
+
title: `chore(release): backfill changesets (${numbers.map(number => `#${number}`).join(', ')})`,
|
|
184
|
+
body: `This PR backfills automated release intents for merged PRs.\n\n${entries.map(entry => `- ${entry.path}`).join('\n')}`,
|
|
185
|
+
})
|
|
186
|
+
return { results, pullRequest: created }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function main() {
|
|
190
|
+
const eventPath = process.env.GITHUB_EVENT_PATH
|
|
191
|
+
const event = eventPath ? JSON.parse(await fs.readFile(eventPath, 'utf8')) : undefined
|
|
192
|
+
const repository = process.env.GITHUB_REPOSITORY ?? event?.repository?.full_name
|
|
193
|
+
const token = process.env.GITHUB_TOKEN
|
|
194
|
+
if (!repository || !token) {
|
|
195
|
+
throw new Error('GITHUB_REPOSITORY and GITHUB_TOKEN are required')
|
|
196
|
+
}
|
|
197
|
+
const client = createGitHubClient({ token, repository })
|
|
198
|
+
if (process.env.GITHUB_EVENT_NAME === 'workflow_dispatch') {
|
|
199
|
+
const numbers = (process.env.PR_NUMBERS ?? '').split(',').map(value => Number(value.trim())).filter(value => Number.isInteger(value) && value > 0)
|
|
200
|
+
if (!numbers.length) {
|
|
201
|
+
throw new Error('PR_NUMBERS must contain at least one PR number')
|
|
202
|
+
}
|
|
203
|
+
console.log(JSON.stringify(await backfillPullRequests(client, numbers, { runId: process.env.GITHUB_RUN_ID })))
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
const number = event?.pull_request?.number
|
|
207
|
+
if (!number) {
|
|
208
|
+
throw new Error('pull_request event payload is required')
|
|
209
|
+
}
|
|
210
|
+
console.log(JSON.stringify(await processPullRequest(client, number)))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
214
|
+
main().catch((error) => {
|
|
215
|
+
console.error(error.stack ?? error)
|
|
216
|
+
process.exitCode = 1
|
|
217
|
+
})
|
|
218
|
+
}
|
|
@@ -5,6 +5,11 @@ on:
|
|
|
5
5
|
# branches: ['main']
|
|
6
6
|
pull_request:
|
|
7
7
|
types: [opened, synchronize]
|
|
8
|
+
paths-ignore:
|
|
9
|
+
- '**/*.md'
|
|
10
|
+
- 'docs/**'
|
|
11
|
+
- '.changeset/**'
|
|
12
|
+
- 'LICENSE*'
|
|
8
13
|
workflow_dispatch:
|
|
9
14
|
|
|
10
15
|
permissions:
|
|
@@ -43,6 +48,12 @@ jobs:
|
|
|
43
48
|
- name: Install dependencies
|
|
44
49
|
run: pnpm install --frozen-lockfile
|
|
45
50
|
|
|
51
|
+
- name: Check tracked build artifacts
|
|
52
|
+
run: pnpm check:no-tracked-build-artifacts
|
|
53
|
+
|
|
54
|
+
- name: Check CI/CD workflow contracts
|
|
55
|
+
run: pnpm check:workflows
|
|
56
|
+
|
|
46
57
|
- name: Security Audit
|
|
47
58
|
run: pnpm audit --audit-level=moderate
|
|
48
59
|
continue-on-error: true
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: Automatic Release Intent
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request_target:
|
|
5
|
+
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
inputs:
|
|
8
|
+
pr_numbers:
|
|
9
|
+
description: Comma-separated merged PR numbers to backfill
|
|
10
|
+
required: true
|
|
11
|
+
type: string
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: write
|
|
15
|
+
pull-requests: write
|
|
16
|
+
|
|
17
|
+
concurrency:
|
|
18
|
+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
19
|
+
cancel-in-progress: false
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
generate:
|
|
23
|
+
name: Generate release intent
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
steps:
|
|
26
|
+
- name: Check out workflow source
|
|
27
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
28
|
+
with:
|
|
29
|
+
fetch-depth: 1
|
|
30
|
+
|
|
31
|
+
- name: Setup Node.js environment
|
|
32
|
+
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
33
|
+
with:
|
|
34
|
+
node-version: 24
|
|
35
|
+
|
|
36
|
+
- name: Generate or backfill changesets
|
|
37
|
+
env:
|
|
38
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
39
|
+
PR_NUMBERS: ${{ inputs.pr_numbers }}
|
|
40
|
+
run: node .github/auto-changeset/index.mjs
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
name: Release Intent Check
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [opened, synchronize, reopened, ready_for_review]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
release-intent:
|
|
12
|
+
name: Require release intent for publishable changes
|
|
13
|
+
if: ${{ !github.event.pull_request.draft }}
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- name: Checkout repository
|
|
17
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
18
|
+
with:
|
|
19
|
+
fetch-depth: 0
|
|
20
|
+
|
|
21
|
+
- name: Skip generated release PRs
|
|
22
|
+
if: startsWith(github.head_ref, 'pnpm-release/') || startsWith(github.head_ref, 'release/pnpm-version')
|
|
23
|
+
run: echo "Generated pnpm release PR detected; skipping validation"
|
|
24
|
+
|
|
25
|
+
- name: Validate release intent presence
|
|
26
|
+
if: ${{ !startsWith(github.head_ref, 'pnpm-release/') && !startsWith(github.head_ref, 'release/pnpm-version') }}
|
|
27
|
+
shell: bash
|
|
28
|
+
run: |
|
|
29
|
+
set -euo pipefail
|
|
30
|
+
|
|
31
|
+
base_sha="${{ github.event.pull_request.base.sha }}"
|
|
32
|
+
head_sha="${{ github.event.pull_request.head.sha }}"
|
|
33
|
+
changed_files="$(git diff --name-only "$base_sha" "$head_sha")"
|
|
34
|
+
|
|
35
|
+
publishable_changes="$(printf '%s\n' "$changed_files" | \
|
|
36
|
+
rg '^packages/[^/]+/' \
|
|
37
|
+
| rg -v '/dist/' \
|
|
38
|
+
|| true)"
|
|
39
|
+
|
|
40
|
+
if [ -z "$publishable_changes" ]; then
|
|
41
|
+
echo "No publishable package changes detected"
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
release_intents="$(printf '%s\n' "$changed_files" | rg '^\.changeset/.+\.md$' || true)"
|
|
46
|
+
|
|
47
|
+
if [ -n "$release_intents" ]; then
|
|
48
|
+
echo "pnpm release intent detected"
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
echo "Publishable package changes require a pnpm release intent in .changeset/*.md."
|
|
53
|
+
echo "Create one with: pnpm change"
|
|
54
|
+
echo
|
|
55
|
+
echo "Changed files:"
|
|
56
|
+
printf '%s\n' "$publishable_changes"
|
|
57
|
+
exit 1
|
package/assets/CONTRIBUTING.md
CHANGED
|
@@ -6,7 +6,7 @@ repoctl accepts bug fixes, documentation improvements, tests, and focused featur
|
|
|
6
6
|
|
|
7
7
|
## Development setup
|
|
8
8
|
|
|
9
|
-
1. Use Node.js 22.
|
|
9
|
+
1. Use Node.js 22.13+ and enable Corepack.
|
|
10
10
|
2. Run `pnpm install`.
|
|
11
11
|
3. Create a focused branch from `main`.
|
|
12
12
|
4. Add or update tests with the implementation.
|
|
@@ -23,6 +23,12 @@ pnpm test
|
|
|
23
23
|
|
|
24
24
|
Changes to publishable packages require a pnpm change intent created with `pnpm change`. Commits must use Conventional Commit syntax.
|
|
25
25
|
|
|
26
|
+
Pull requests that change a publishable package receive an automatic `.changeset/auto-pr-<number>.md` intent when they come from a branch in this repository. The default bump is `patch`; add the `release:minor` or `release:major` label to override it. Fork pull requests cannot be written to by the workflow, so add the intent manually. Existing hand-written intents are always preserved.
|
|
27
|
+
|
|
28
|
+
To backfill merged pull requests that predate this automation, run the `Automatic Release Intent` workflow manually with a comma-separated list of PR numbers. The workflow opens one backfill pull request, and the next `repo release ci` run consumes its changesets normally.
|
|
29
|
+
|
|
30
|
+
The repository includes repoctl's CLI and monorepo engine, template assets, shared lint/config packages, and the `apps/mock` fixture demo. Keep package APIs and independent version lines intact when working across these areas.
|
|
31
|
+
|
|
26
32
|
When changing templates or managed root assets, update their source first and refresh packaged copies with `pnpm --filter @icebreakers/monorepo-templates sync:assets`.
|
|
27
33
|
|
|
28
34
|
Report bugs at https://github.com/sonofmagic/repoctl/issues and use discussions for design questions that do not yet have a concrete implementation.
|
package/assets/package.json
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"type": "module",
|
|
4
4
|
"version": "0.0.0",
|
|
5
5
|
"private": true,
|
|
6
|
-
"packageManager": "pnpm@11.
|
|
7
|
-
"description": "Source workspace for repoctl
|
|
6
|
+
"packageManager": "pnpm@11.25.0",
|
|
7
|
+
"description": "Source workspace for repoctl and Icebreaker's shared development packages",
|
|
8
8
|
"author": "ice breaker <hi@sonofmagic.top>",
|
|
9
9
|
"homepage": "https://repoctl.icebreaker.top",
|
|
10
10
|
"repository": {
|
|
@@ -22,17 +22,20 @@
|
|
|
22
22
|
"typescript"
|
|
23
23
|
],
|
|
24
24
|
"engines": {
|
|
25
|
-
"node": ">=22.
|
|
25
|
+
"node": ">=22.13.0"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"build": "turbo run build",
|
|
29
29
|
"tooling:build": "turbo run build --filter=@icebreakers/monorepo --filter=repoctl",
|
|
30
30
|
"dev": "turbo run dev --parallel",
|
|
31
|
-
"test": "vitest run",
|
|
31
|
+
"test": "node -e \"require('node:fs').mkdirSync('coverage/.tmp', { recursive: true })\" && pnpm check:no-tracked-build-artifacts && vitest run && pnpm test:types",
|
|
32
32
|
"test:packaged-doctor": "node scripts/smoke-packaged-doctor.mjs",
|
|
33
|
-
"test:dev": "vitest",
|
|
33
|
+
"test:dev": "node -e \"require('node:fs').mkdirSync('coverage/.tmp', { recursive: true })\" && pnpm check:no-tracked-build-artifacts && vitest",
|
|
34
34
|
"lint": "pnpm run tooling:build && turbo run lint",
|
|
35
35
|
"tsd": "turbo run tsd",
|
|
36
|
+
"test:types": "turbo run test:types",
|
|
37
|
+
"check:no-tracked-build-artifacts": "node ./scripts/check-no-tracked-build-artifacts.mjs",
|
|
38
|
+
"check:workflows": "node ./scripts/check-workflows.mjs",
|
|
36
39
|
"audit": "pnpm audit --audit-level=moderate",
|
|
37
40
|
"audit:fix": "pnpm audit --fix",
|
|
38
41
|
"cf-typegen": "pnpm --filter @icebreakers/client --filter @icebreakers/server run cf-typegen",
|
|
@@ -47,8 +50,12 @@
|
|
|
47
50
|
"------": "--------------",
|
|
48
51
|
"preinstall": "npx only-allow pnpm",
|
|
49
52
|
"prepare": "husky",
|
|
53
|
+
"postinstall": "pnpm run build",
|
|
50
54
|
"commit": "commit",
|
|
51
55
|
"commitlint": "commitlint --edit",
|
|
56
|
+
"sync": "turbo run sync",
|
|
57
|
+
"inspector": "npx @eslint/config-inspector",
|
|
58
|
+
"build:inspector": "pnpm build && npx @eslint/config-inspector build",
|
|
52
59
|
"-------": "---------------",
|
|
53
60
|
"repo:init": "repo init",
|
|
54
61
|
"repo:new": "repo new",
|
|
@@ -64,31 +71,67 @@
|
|
|
64
71
|
},
|
|
65
72
|
"devDependencies": {
|
|
66
73
|
"@commitlint/cli": "^21.2.2",
|
|
74
|
+
"@eslint/config-inspector": "^3.4.0",
|
|
75
|
+
"@eslint/eslintrc": "^3.3.7",
|
|
76
|
+
"@icebreakers/commitlint-config": "workspace:*",
|
|
77
|
+
"@icebreakers/eslint-config": "workspace:*",
|
|
78
|
+
"@icebreakers/stylelint-config": "workspace:*",
|
|
79
|
+
"@next/eslint-plugin-next": "^16.3.4",
|
|
80
|
+
"@prettier/plugin-xml": "^3.4.2",
|
|
81
|
+
"@tanstack/eslint-plugin-query": "^5.102.8",
|
|
82
|
+
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
|
83
|
+
"@types/fs-extra": "^11.0.4",
|
|
84
|
+
"@types/lodash": "^4.17.25",
|
|
85
|
+
"@types/lodash-es": "^4.17.12",
|
|
86
|
+
"@types/micromatch": "^4.0.10",
|
|
67
87
|
"@icebreakers/monorepo": "workspace:*",
|
|
68
|
-
"@types/node": "^26.
|
|
88
|
+
"@types/node": "^26.4.1",
|
|
69
89
|
"@types/semver": "^7.8.0",
|
|
70
|
-
"@
|
|
90
|
+
"@types/set-value": "^4.0.3",
|
|
91
|
+
"@typescript-eslint/types": "^8.69.0",
|
|
92
|
+
"@unocss/eslint-plugin": "66.9.2",
|
|
93
|
+
"@vitest/coverage-v8": "~5.0.0",
|
|
94
|
+
"astro-eslint-parser": "^3.1.0",
|
|
71
95
|
"ci-info": "^4.4.0",
|
|
72
96
|
"cross-env": "^10.1.0",
|
|
73
97
|
"defu": "^6.1.7",
|
|
74
|
-
"es-toolkit": "^1.
|
|
98
|
+
"es-toolkit": "^1.52.0",
|
|
75
99
|
"eslint": "^10.9.1",
|
|
100
|
+
"eslint-plugin-astro": "^3.1.0",
|
|
101
|
+
"eslint-plugin-mdx": "3.8.1",
|
|
102
|
+
"eslint-plugin-svelte": "^3.23.0",
|
|
103
|
+
"execa": "^10.0.1",
|
|
104
|
+
"fast-glob": "^3.3.3",
|
|
105
|
+
"fs-extra": "^11.4.0",
|
|
106
|
+
"get-value": "^4.1.0",
|
|
76
107
|
"husky": "^9.1.7",
|
|
77
|
-
"
|
|
108
|
+
"jiti": "^2.7.0",
|
|
109
|
+
"lint-staged": "^17.4.1",
|
|
110
|
+
"lodash": "^4.18.1",
|
|
111
|
+
"lodash-es": "^4.18.1",
|
|
112
|
+
"micromatch": "^4.0.8",
|
|
78
113
|
"only-allow": "^1.2.2",
|
|
79
114
|
"pathe": "^2.0.3",
|
|
80
|
-
"pkg-types": "^2.3.
|
|
115
|
+
"pkg-types": "^2.3.2",
|
|
116
|
+
"prettier": "^3.9.6",
|
|
117
|
+
"prettier-plugin-astro": "^0.14.1",
|
|
81
118
|
"repoctl": "workspace:*",
|
|
119
|
+
"rimraf": "^6.1.3",
|
|
120
|
+
"set-value": "^4.1.0",
|
|
82
121
|
"stylelint": "^17.14.1",
|
|
122
|
+
"svelte": "^5.57.0",
|
|
123
|
+
"svelte-eslint-parser": "^1.8.1",
|
|
83
124
|
"tailwindcss": "^4.3.3",
|
|
84
125
|
"tsd": "^0.33.0",
|
|
85
126
|
"tsdown": "0.22.14",
|
|
86
127
|
"tslib": "^2.8.1",
|
|
87
|
-
"tsx": "^4.23.
|
|
88
|
-
"turbo": "^2.10.
|
|
89
|
-
"type-fest": "^5.
|
|
128
|
+
"tsx": "^4.23.13",
|
|
129
|
+
"turbo": "^2.10.12",
|
|
130
|
+
"type-fest": "^5.9.0",
|
|
131
|
+
"tsup": "^8.5.1",
|
|
90
132
|
"typescript": "^6.0.3",
|
|
91
|
-
"
|
|
133
|
+
"unbuild": "^3.6.1",
|
|
134
|
+
"vitest": "~5.0.0",
|
|
92
135
|
"yaml": "^2.9.0"
|
|
93
136
|
},
|
|
94
137
|
"publishConfig": {
|
|
@@ -14,3 +14,16 @@ allowBuilds:
|
|
|
14
14
|
sharp: true
|
|
15
15
|
vue-demi: true
|
|
16
16
|
workerd: true
|
|
17
|
+
overrides:
|
|
18
|
+
prettier: 3.9.6
|
|
19
|
+
typescript: 6.0.3
|
|
20
|
+
vite: 8.2.2
|
|
21
|
+
postcss: 8.5.28
|
|
22
|
+
postcss-selector-parser: 7.1.6
|
|
23
|
+
js-yaml: 5.4.1
|
|
24
|
+
nanoid: 6.0.1
|
|
25
|
+
brace-expansion: 5.0.9
|
|
26
|
+
cross-spawn: 7.0.6
|
|
27
|
+
adm-zip: 0.6.0
|
|
28
|
+
uuid: 14.0.2
|
|
29
|
+
esbuild: 0.28.2
|
package/assets/repoctl.config.ts
CHANGED
|
@@ -20,10 +20,15 @@ export default {
|
|
|
20
20
|
extends: ['@commitlint/config-conventional'],
|
|
21
21
|
},
|
|
22
22
|
eslint: {
|
|
23
|
+
astro: true,
|
|
23
24
|
ignores: ['**/fixtures/**'],
|
|
25
|
+
svelte: true,
|
|
26
|
+
vue: true,
|
|
24
27
|
},
|
|
25
28
|
stylelint: {
|
|
26
|
-
rules: {
|
|
29
|
+
rules: {
|
|
30
|
+
'media-feature-range-notation': 'prefix',
|
|
31
|
+
},
|
|
27
32
|
},
|
|
28
33
|
lintStaged: {
|
|
29
34
|
monorepoCommand: 'pnpm exec repo',
|
package/assets/turbo.json
CHANGED
|
@@ -52,6 +52,19 @@
|
|
|
52
52
|
"coverage/**"
|
|
53
53
|
]
|
|
54
54
|
},
|
|
55
|
+
"test:types": {
|
|
56
|
+
"dependsOn": [
|
|
57
|
+
"build",
|
|
58
|
+
"^build"
|
|
59
|
+
],
|
|
60
|
+
"inputs": [
|
|
61
|
+
"test-d/**",
|
|
62
|
+
"package.json",
|
|
63
|
+
"tsconfig*.json",
|
|
64
|
+
"dist/**"
|
|
65
|
+
],
|
|
66
|
+
"outputs": []
|
|
67
|
+
},
|
|
55
68
|
"typecheck": {
|
|
56
69
|
"dependsOn": [
|
|
57
70
|
"^typecheck"
|