@goodandready/dsh-github-ops 0.6.8 → 0.6.9
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/CHANGELOG.md +6 -0
- package/lib/client.js +58 -2
- package/lib/routes.js +7 -0
- package/lib/tools/changelog.js +261 -0
- package/lib/tools/community.js +238 -0
- package/lib/tools/register-core.js +26 -0
- package/lib/tools/register-write.js +37 -0
- package/lib/tools/write-files.js +14 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.9
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
- **Conventional Changelog Generator (#123)**: Added `gh_changelog_generate` tool parsing commit and PR history according to Conventional Commits standard (`feat:`, `fix:`, `perf:`, `refactor:`, `chore:`, etc.), detecting Breaking Changes (`!` and `BREAKING CHANGE:`), recommending SemVer bump (`major`, `minor`, `patch`), and outputting structured JSON and GitHub/CHANGELOG markdown. Added Changelog tab in repository panel UI.
|
|
7
|
+
- **GoodAndReady Community Scaffold (#126)**: Added `gh_community_scaffold` tool generating standardized community health files (`.github/ISSUE_TEMPLATE` bug report, feature request, security advisory; 5-section `.github/PULL_REQUEST_TEMPLATE.md`; `SECURITY.md`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`). Integrated `includeCommunityFiles` option into `gh_upload_project` and `uploadProject`.
|
|
8
|
+
|
|
3
9
|
## 0.5.0
|
|
4
10
|
|
|
5
11
|
### Features
|
package/lib/client.js
CHANGED
|
@@ -130,6 +130,9 @@ window.__ModuleLoader__.load({
|
|
|
130
130
|
panelSecurity: 'Security',
|
|
131
131
|
panelSecurityEmpty: 'No security alerts found.',
|
|
132
132
|
panelSecurityDisabled: 'Security features are disabled or token lacks security scope.',
|
|
133
|
+
panelChangelog: 'Changelog',
|
|
134
|
+
panelChangelogEmpty: 'No conventional changelog entries found.',
|
|
135
|
+
panelCopyMarkdown: 'Copy Markdown',
|
|
133
136
|
panelDiagnose: 'Diagnose',
|
|
134
137
|
panelDiagnosing: 'Diagnosing…',
|
|
135
138
|
panelDiagnoseAsk: 'Ask Agent to Diagnose & Fix',
|
|
@@ -257,6 +260,9 @@ window.__ModuleLoader__.load({
|
|
|
257
260
|
panelSecurity: '安全',
|
|
258
261
|
panelSecurityEmpty: '未发现安全警报。',
|
|
259
262
|
panelSecurityDisabled: '安全功能已禁用或令牌缺少安全权限。',
|
|
263
|
+
panelChangelog: '更新日志',
|
|
264
|
+
panelChangelogEmpty: '未找到常规提交更新日志记录。',
|
|
265
|
+
panelCopyMarkdown: '复制 Markdown',
|
|
260
266
|
panelDiagnose: '诊断',
|
|
261
267
|
panelDiagnosing: '诊断中…',
|
|
262
268
|
panelDiagnoseAsk: '请智能体诊断并修复',
|
|
@@ -870,6 +876,7 @@ window.__ModuleLoader__.load({
|
|
|
870
876
|
const [error, setError] = React.useState('')
|
|
871
877
|
const [loading, setLoading] = React.useState(false)
|
|
872
878
|
const [securityData, setSecurityData] = React.useState({ phase: 'idle', data: null })
|
|
879
|
+
const [changelogData, setChangelogData] = React.useState({ phase: 'idle', data: null })
|
|
873
880
|
const [diagnostics, setDiagnostics] = React.useState(null)
|
|
874
881
|
const [diagnoseLoadingId, setDiagnoseLoadingId] = React.useState(null)
|
|
875
882
|
const [expandedSteps, setExpandedSteps] = React.useState({})
|
|
@@ -921,6 +928,14 @@ window.__ModuleLoader__.load({
|
|
|
921
928
|
.catch((err) => { setSecurityData({ phase: 'error', data: null }); setError(String((err && err.message) || err)) })
|
|
922
929
|
}, [call, repo])
|
|
923
930
|
|
|
931
|
+
const loadChangelog = React.useCallback(() => {
|
|
932
|
+
if (!repo) return
|
|
933
|
+
setChangelogData({ phase: 'loading', data: null })
|
|
934
|
+
call({ what: 'changelog', repo })
|
|
935
|
+
.then((payload) => setChangelogData({ phase: 'ready', data: payload }))
|
|
936
|
+
.catch((err) => { setChangelogData({ phase: 'error', data: null }); setError(String((err && err.message) || err)) })
|
|
937
|
+
}, [call, repo])
|
|
938
|
+
|
|
924
939
|
const loadDiagnostics = (runId) => {
|
|
925
940
|
if (!repo || !runId) return
|
|
926
941
|
setDiagnoseLoadingId(runId)
|
|
@@ -957,8 +972,9 @@ window.__ModuleLoader__.load({
|
|
|
957
972
|
else if (tab === 'inbox') loadInbox()
|
|
958
973
|
else if (tab === 'matrix') loadMatrix()
|
|
959
974
|
else if (tab === 'security') loadSecurity()
|
|
975
|
+
else if (tab === 'changelog') loadChangelog()
|
|
960
976
|
else loadList(tab)
|
|
961
|
-
}, [repo, tab, loadTree, loadList, loadInbox, loadSecurity])
|
|
977
|
+
}, [repo, tab, loadTree, loadList, loadInbox, loadSecurity, loadChangelog])
|
|
962
978
|
|
|
963
979
|
const openFile = (entry) => {
|
|
964
980
|
if (entry.type === 'tree') { loadTree(entry.path); return }
|
|
@@ -1143,6 +1159,45 @@ window.__ModuleLoader__.load({
|
|
|
1143
1159
|
)
|
|
1144
1160
|
})()
|
|
1145
1161
|
|
|
1162
|
+
const changelogView = (() => {
|
|
1163
|
+
if (changelogData.phase === 'loading') return React.createElement('div', { className: 'gho-panel-muted', style: { padding: '8px 0' } }, t('panelLoading'))
|
|
1164
|
+
const data = changelogData.data
|
|
1165
|
+
if (!data || !data.markdown) return React.createElement('div', { className: 'gho-panel-muted', style: { padding: '8px 0' } }, t('panelChangelogEmpty'))
|
|
1166
|
+
|
|
1167
|
+
return React.createElement('div', { style: { padding: '4px 0' } },
|
|
1168
|
+
React.createElement('div', { style: { display: 'flex', gap: '8px', alignItems: 'center', margin: '4px 0 8px' } },
|
|
1169
|
+
React.createElement('span', { className: 'gho-badge gho-badge-active' }, `Bump: ${data.recommendedBump || 'none'}`),
|
|
1170
|
+
React.createElement('span', { style: { fontWeight: 600, fontSize: '13px' } }, data.version || ''),
|
|
1171
|
+
React.createElement('button', {
|
|
1172
|
+
type: 'button',
|
|
1173
|
+
className: 'gho-diagnose-btn',
|
|
1174
|
+
style: { marginLeft: 'auto' },
|
|
1175
|
+
onClick: () => {
|
|
1176
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.writeText) {
|
|
1177
|
+
navigator.clipboard.writeText(data.markdown || '').catch(() => {})
|
|
1178
|
+
}
|
|
1179
|
+
setCopiedPrompt(true)
|
|
1180
|
+
setTimeout(() => setCopiedPrompt(false), 3000)
|
|
1181
|
+
},
|
|
1182
|
+
}, copiedPrompt ? t('panelDiagnoseCopied') : t('panelCopyMarkdown')),
|
|
1183
|
+
),
|
|
1184
|
+
React.createElement('pre', {
|
|
1185
|
+
style: {
|
|
1186
|
+
whiteSpace: 'pre-wrap',
|
|
1187
|
+
fontFamily: 'monospace',
|
|
1188
|
+
fontSize: '11px',
|
|
1189
|
+
lineHeight: '1.4',
|
|
1190
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1191
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
1192
|
+
borderRadius: '6px',
|
|
1193
|
+
padding: '8px',
|
|
1194
|
+
maxHeight: '360px',
|
|
1195
|
+
overflow: 'auto',
|
|
1196
|
+
},
|
|
1197
|
+
}, data.markdown),
|
|
1198
|
+
)
|
|
1199
|
+
})()
|
|
1200
|
+
|
|
1146
1201
|
return React.createElement('div', { className: 'gho-panel' },
|
|
1147
1202
|
header,
|
|
1148
1203
|
React.createElement('div', { className: 'gho-panel-tabs' },
|
|
@@ -1151,12 +1206,13 @@ window.__ModuleLoader__.load({
|
|
|
1151
1206
|
tabButton('pulls', t('panelPulls')),
|
|
1152
1207
|
tabButton('runs', t('panelRuns')),
|
|
1153
1208
|
tabButton('security', t('panelSecurity')),
|
|
1209
|
+
tabButton('changelog', t('panelChangelog')),
|
|
1154
1210
|
tabButton('inbox', t('panelInbox')),
|
|
1155
1211
|
tabButton('matrix', t('panelMatrix')),
|
|
1156
1212
|
React.createElement('button', { type: 'button', className: 'gho-panel-tab', onClick: () => { setRepo(''); setFile(null) } }, t('panelRepo')),
|
|
1157
1213
|
),
|
|
1158
1214
|
error ? React.createElement('div', { className: 'gho-panel-muted' }, error) : null,
|
|
1159
|
-
tab === 'tree' ? treeView : (tab === 'matrix' ? matrixView : (tab === 'security' ? securityView : list)),
|
|
1215
|
+
tab === 'tree' ? treeView : (tab === 'matrix' ? matrixView : (tab === 'security' ? securityView : (tab === 'changelog' ? changelogView : list))),
|
|
1160
1216
|
)
|
|
1161
1217
|
}
|
|
1162
1218
|
|
package/lib/routes.js
CHANGED
|
@@ -16,6 +16,7 @@ import { encodeRepoPath } from './github.js'
|
|
|
16
16
|
import { listIssues } from './tools/issues.js'
|
|
17
17
|
import { listRuns, diagnoseRun } from './tools/actions.js'
|
|
18
18
|
import { listSecurityAlerts } from './tools/security.js'
|
|
19
|
+
import { generateChangelog } from './tools/changelog.js'
|
|
19
20
|
import { listNotifications, repoTreeView } from './tools/panel-data.js'
|
|
20
21
|
import { mirrorStatus, syncMirror } from './tools/mirror.js'
|
|
21
22
|
import { matrixStatus } from './tools/matrix.js'
|
|
@@ -413,6 +414,12 @@ export function registerRoutes(ctx, deps = {}) {
|
|
|
413
414
|
const result = await listRuns(github, { ...repo, limit: 20 })
|
|
414
415
|
return sendJson(res, 200, { configured: true, items: result.runs })
|
|
415
416
|
}
|
|
417
|
+
if (what === 'changelog') {
|
|
418
|
+
const base = url.searchParams.get('base') || undefined
|
|
419
|
+
const head = url.searchParams.get('head') || 'HEAD'
|
|
420
|
+
const result = await generateChangelog(github, { ...repo, base, head })
|
|
421
|
+
return sendJson(res, 200, { configured: true, ...result })
|
|
422
|
+
}
|
|
416
423
|
if (what === 'security') {
|
|
417
424
|
const severity = url.searchParams.get('severity') || undefined
|
|
418
425
|
const result = await listSecurityAlerts(github, { ...repo, state, severity, limit: 50 })
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// Conventional changelog generation and SemVer recommendation tools.
|
|
2
|
+
// Pure functions over GitHub client or commit arrays.
|
|
3
|
+
|
|
4
|
+
export const CATEGORIES = {
|
|
5
|
+
breaking: { key: 'breaking', title: '⚠️ Breaking Changes', order: 1 },
|
|
6
|
+
feat: { key: 'feat', title: '🚀 Features', order: 2 },
|
|
7
|
+
fix: { key: 'fix', title: '🐛 Bug Fixes', order: 3 },
|
|
8
|
+
perf: { key: 'perf', title: '⚡ Performance Improvements', order: 4 },
|
|
9
|
+
refactor: { key: 'refactor', title: '♻️ Code Refactoring', order: 5 },
|
|
10
|
+
docs: { key: 'docs', title: '📚 Documentation', order: 6 },
|
|
11
|
+
test: { key: 'test', title: '🧪 Tests', order: 7 },
|
|
12
|
+
maintenance: { key: 'maintenance', title: '🛠️ Maintenance', order: 8 },
|
|
13
|
+
other: { key: 'other', title: '📦 Other Changes', order: 9 },
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const TYPE_TO_CATEGORY = {
|
|
17
|
+
feat: 'feat',
|
|
18
|
+
feature: 'feat',
|
|
19
|
+
fix: 'fix',
|
|
20
|
+
bugfix: 'fix',
|
|
21
|
+
perf: 'perf',
|
|
22
|
+
refactor: 'refactor',
|
|
23
|
+
docs: 'docs',
|
|
24
|
+
doc: 'docs',
|
|
25
|
+
test: 'test',
|
|
26
|
+
tests: 'test',
|
|
27
|
+
chore: 'maintenance',
|
|
28
|
+
ci: 'maintenance',
|
|
29
|
+
build: 'maintenance',
|
|
30
|
+
deps: 'maintenance',
|
|
31
|
+
style: 'maintenance',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const CONVENTIONAL_RE = /^([a-zA-Z]+)(?:\(([^)]+)\))?(!)?:\s*(.+)$/
|
|
35
|
+
|
|
36
|
+
/** Parse a single commit message into conventional commit structure. */
|
|
37
|
+
export function parseConventionalCommit(rawMessage, sha = '') {
|
|
38
|
+
const full = String(rawMessage || '').trim()
|
|
39
|
+
const lines = full.split('\n').map((l) => l.trim())
|
|
40
|
+
const header = lines[0] || ''
|
|
41
|
+
const body = lines.slice(1).join('\n')
|
|
42
|
+
|
|
43
|
+
let type = 'other'
|
|
44
|
+
let scope = ''
|
|
45
|
+
let isBreaking = false
|
|
46
|
+
let subject = header
|
|
47
|
+
let breakingNote = ''
|
|
48
|
+
|
|
49
|
+
// Check for BREAKING CHANGE in body or footer
|
|
50
|
+
const breakingMatch = body.match(/BREAKING[- ]CHANGE:\s*([^\n]+)/i)
|
|
51
|
+
if (breakingMatch) {
|
|
52
|
+
isBreaking = true
|
|
53
|
+
breakingNote = breakingMatch[1].trim()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const match = header.match(CONVENTIONAL_RE)
|
|
57
|
+
if (match) {
|
|
58
|
+
type = match[1].toLowerCase()
|
|
59
|
+
scope = match[2] ? match[2].trim() : ''
|
|
60
|
+
if (match[3] === '!') isBreaking = true
|
|
61
|
+
subject = match[4].trim()
|
|
62
|
+
} else {
|
|
63
|
+
// Handle typical merge pull request headers e.g. "Merge pull request #123 from org/branch"
|
|
64
|
+
const prMatch = header.match(/^Merge pull request #(\d+)/i)
|
|
65
|
+
if (prMatch) {
|
|
66
|
+
const candidateLine = lines.slice(1).find((l) => l.trim().length > 0)
|
|
67
|
+
if (candidateLine) {
|
|
68
|
+
const subMatch = candidateLine.match(CONVENTIONAL_RE)
|
|
69
|
+
if (subMatch) {
|
|
70
|
+
type = subMatch[1].toLowerCase()
|
|
71
|
+
scope = subMatch[2] ? subMatch[2].trim() : ''
|
|
72
|
+
if (subMatch[3] === '!') isBreaking = true
|
|
73
|
+
subject = `${subMatch[4].trim()} (#${prMatch[1]})`
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const categoryKey = isBreaking
|
|
80
|
+
? 'breaking'
|
|
81
|
+
: (TYPE_TO_CATEGORY[type] || 'other')
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
sha: sha ? sha.slice(0, 7) : '',
|
|
85
|
+
fullSha: sha || '',
|
|
86
|
+
raw: full,
|
|
87
|
+
header,
|
|
88
|
+
type,
|
|
89
|
+
scope,
|
|
90
|
+
subject,
|
|
91
|
+
isBreaking,
|
|
92
|
+
breakingNote,
|
|
93
|
+
category: categoryKey,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Calculate recommended SemVer bump from parsed commits. */
|
|
98
|
+
export function calculateBump(commits = []) {
|
|
99
|
+
if (!Array.isArray(commits) || commits.length === 0) return 'none'
|
|
100
|
+
if (commits.some((c) => c.isBreaking || c.category === 'breaking')) return 'major'
|
|
101
|
+
if (commits.some((c) => c.type === 'feat' || c.category === 'feat')) return 'minor'
|
|
102
|
+
return 'patch'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Bump a SemVer version string according to bump type ('major', 'minor', 'patch'). */
|
|
106
|
+
export function bumpVersion(version, bump) {
|
|
107
|
+
if (!version || bump === 'none') return version || '0.1.0'
|
|
108
|
+
const hasV = version.startsWith('v')
|
|
109
|
+
const clean = hasV ? version.slice(1) : version
|
|
110
|
+
const parts = clean.split('-')[0].split('.').map(Number)
|
|
111
|
+
let [major = 0, minor = 0, patch = 0] = parts
|
|
112
|
+
|
|
113
|
+
if (bump === 'major') {
|
|
114
|
+
major += 1
|
|
115
|
+
minor = 0
|
|
116
|
+
patch = 0
|
|
117
|
+
} else if (bump === 'minor') {
|
|
118
|
+
minor += 1
|
|
119
|
+
patch = 0
|
|
120
|
+
} else if (bump === 'patch') {
|
|
121
|
+
patch += 1
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const res = `${major}.${minor}.${patch}`
|
|
125
|
+
return hasV ? `v${res}` : res
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Format parsed commits into structured markdown changelog. */
|
|
129
|
+
export function formatChangelog({ version = '', date = '', sections = {}, compareUrl = '' } = {}) {
|
|
130
|
+
const headerDate = date || new Date().toISOString().slice(0, 10)
|
|
131
|
+
const lines = []
|
|
132
|
+
if (version) {
|
|
133
|
+
lines.push(`## [${version}] - ${headerDate}`)
|
|
134
|
+
} else {
|
|
135
|
+
lines.push(`## Changelog - ${headerDate}`)
|
|
136
|
+
}
|
|
137
|
+
lines.push('')
|
|
138
|
+
|
|
139
|
+
const sortedCategories = Object.values(CATEGORIES).sort((a, b) => a.order - b.order)
|
|
140
|
+
|
|
141
|
+
let hasEntries = false
|
|
142
|
+
for (const cat of sortedCategories) {
|
|
143
|
+
const list = sections[cat.key] || []
|
|
144
|
+
if (list.length === 0) continue
|
|
145
|
+
hasEntries = true
|
|
146
|
+
lines.push(`### ${cat.title}`)
|
|
147
|
+
for (const item of list) {
|
|
148
|
+
const scopePart = item.scope ? `**${item.scope}**: ` : ''
|
|
149
|
+
const shaPart = item.sha ? ` (${item.sha})` : ''
|
|
150
|
+
const breakingPrefix = (cat.key === 'breaking' && item.breakingNote) ? `*${item.breakingNote}* — ` : ''
|
|
151
|
+
lines.push(`- ${scopePart}${breakingPrefix}${item.subject}${shaPart}`)
|
|
152
|
+
}
|
|
153
|
+
lines.push('')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!hasEntries) {
|
|
157
|
+
lines.push('*No significant user-facing changes.*')
|
|
158
|
+
lines.push('')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (compareUrl) {
|
|
162
|
+
lines.push(`[Full Diff](${compareUrl})`)
|
|
163
|
+
lines.push('')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return lines.join('\n').trim()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** High level changelog generator. */
|
|
170
|
+
export async function generateChangelog(client, {
|
|
171
|
+
owner, repo, base, head = 'HEAD', version, date, commits: customCommits,
|
|
172
|
+
} = {}) {
|
|
173
|
+
let commitList = []
|
|
174
|
+
let compareUrl = ''
|
|
175
|
+
let previousTag = base || ''
|
|
176
|
+
|
|
177
|
+
if (Array.isArray(customCommits)) {
|
|
178
|
+
commitList = customCommits.map((c) => {
|
|
179
|
+
if (typeof c === 'string') return { message: c, sha: '' }
|
|
180
|
+
return { message: c.message || c.commit?.message || '', sha: c.sha || '' }
|
|
181
|
+
})
|
|
182
|
+
} else if (client && owner && repo) {
|
|
183
|
+
if (!previousTag) {
|
|
184
|
+
try {
|
|
185
|
+
const { data: tags } = await client.get(`/repos/${owner}/${repo}/tags`, { query: { per_page: 1 } })
|
|
186
|
+
if (Array.isArray(tags) && tags.length > 0) {
|
|
187
|
+
previousTag = tags[0].name
|
|
188
|
+
}
|
|
189
|
+
} catch {
|
|
190
|
+
// ignore tag fetch failure
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (previousTag) {
|
|
195
|
+
try {
|
|
196
|
+
const { data } = await client.get(`/repos/${owner}/${repo}/compare/${encodeURIComponent(previousTag)}...${encodeURIComponent(head)}`)
|
|
197
|
+
compareUrl = data?.html_url || ''
|
|
198
|
+
const rawCommits = data?.commits || []
|
|
199
|
+
commitList = rawCommits.map((c) => ({
|
|
200
|
+
message: c.commit?.message || '',
|
|
201
|
+
sha: c.sha || '',
|
|
202
|
+
author: c.author?.login || c.commit?.author?.name || '',
|
|
203
|
+
}))
|
|
204
|
+
} catch {
|
|
205
|
+
commitList = []
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
try {
|
|
209
|
+
const { data } = await client.get(`/repos/${owner}/${repo}/commits`, { query: { per_page: 50, sha: head } })
|
|
210
|
+
const rawCommits = Array.isArray(data) ? data : []
|
|
211
|
+
commitList = rawCommits.map((c) => ({
|
|
212
|
+
message: c.commit?.message || '',
|
|
213
|
+
sha: c.sha || '',
|
|
214
|
+
author: c.author?.login || c.commit?.author?.name || '',
|
|
215
|
+
}))
|
|
216
|
+
} catch {
|
|
217
|
+
commitList = []
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const parsed = commitList.map((c) => parseConventionalCommit(c.message, c.sha))
|
|
223
|
+
const sections = {}
|
|
224
|
+
for (const catKey of Object.keys(CATEGORIES)) {
|
|
225
|
+
sections[catKey] = []
|
|
226
|
+
}
|
|
227
|
+
for (const item of parsed) {
|
|
228
|
+
const key = item.category in sections ? item.category : 'other'
|
|
229
|
+
sections[key].push(item)
|
|
230
|
+
if (item.isBreaking && item.type in sections && item.type !== 'breaking') {
|
|
231
|
+
sections[item.type].push(item)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const recommendedBump = calculateBump(parsed)
|
|
236
|
+
const nextVersion = version || (previousTag ? bumpVersion(previousTag, recommendedBump) : bumpVersion('0.1.0', recommendedBump))
|
|
237
|
+
const markdown = formatChangelog({ version: nextVersion, date, sections, compareUrl })
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
version: nextVersion,
|
|
241
|
+
previousTag: previousTag || null,
|
|
242
|
+
head,
|
|
243
|
+
recommendedBump,
|
|
244
|
+
commitsCount: parsed.length,
|
|
245
|
+
summary: {
|
|
246
|
+
breaking: sections.breaking.length,
|
|
247
|
+
feat: sections.feat.length,
|
|
248
|
+
fix: sections.fix.length,
|
|
249
|
+
perf: sections.perf.length,
|
|
250
|
+
refactor: sections.refactor.length,
|
|
251
|
+
docs: sections.docs.length,
|
|
252
|
+
test: sections.test.length,
|
|
253
|
+
maintenance: sections.maintenance.length,
|
|
254
|
+
other: sections.other.length,
|
|
255
|
+
total: parsed.length,
|
|
256
|
+
},
|
|
257
|
+
sections,
|
|
258
|
+
markdown,
|
|
259
|
+
commits: parsed,
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// Community health files scaffolding tools.
|
|
2
|
+
// Standardized templates for GoodAndReady open source plugins and repositories.
|
|
3
|
+
|
|
4
|
+
import { pushFiles } from './write-files.js'
|
|
5
|
+
|
|
6
|
+
export const TEMPLATE_KEYS = [
|
|
7
|
+
'bug_report',
|
|
8
|
+
'feature_request',
|
|
9
|
+
'security_advisory',
|
|
10
|
+
'pull_request',
|
|
11
|
+
'security',
|
|
12
|
+
'contributing',
|
|
13
|
+
'code_of_conduct',
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
/** Generate file mapping for requested community templates. */
|
|
17
|
+
export function getCommunityTemplates({
|
|
18
|
+
projectName = 'Project',
|
|
19
|
+
contactEmail = 'security@goodandready.app',
|
|
20
|
+
defaultBranch = 'main',
|
|
21
|
+
supportVersion = '>= 0.6.0',
|
|
22
|
+
} = {}) {
|
|
23
|
+
const templates = {}
|
|
24
|
+
|
|
25
|
+
templates['.github/ISSUE_TEMPLATE/bug_report.md'] = `---
|
|
26
|
+
name: Bug Report
|
|
27
|
+
about: Create a report to help us improve
|
|
28
|
+
title: 'fix: '
|
|
29
|
+
labels: ['bug']
|
|
30
|
+
assignees: ''
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
### 1. Context & Description
|
|
34
|
+
<!-- A clear and concise description of what the bug is. -->
|
|
35
|
+
|
|
36
|
+
### 2. Steps to Reproduce
|
|
37
|
+
1. Go to '...'
|
|
38
|
+
2. Click on '....'
|
|
39
|
+
3. See error
|
|
40
|
+
|
|
41
|
+
### 3. Expected Behavior
|
|
42
|
+
<!-- What you expected to happen. -->
|
|
43
|
+
|
|
44
|
+
### 4. Actual Behavior / Logs
|
|
45
|
+
<!-- Screenshots or error logs if applicable. -->
|
|
46
|
+
|
|
47
|
+
### 5. Environment
|
|
48
|
+
- OS: [e.g. Linux / Windows]
|
|
49
|
+
- Node version: [e.g. 22.x]
|
|
50
|
+
- Plugin version: [e.g. ${supportVersion}]
|
|
51
|
+
`
|
|
52
|
+
|
|
53
|
+
templates['.github/ISSUE_TEMPLATE/feature_request.md'] = `---
|
|
54
|
+
name: Feature Request
|
|
55
|
+
about: Suggest an idea or enhancement
|
|
56
|
+
title: 'feat: '
|
|
57
|
+
labels: ['enhancement']
|
|
58
|
+
assignees: ''
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
### 1. Problem Statement
|
|
62
|
+
<!-- Is your feature request related to a problem? Please describe. -->
|
|
63
|
+
|
|
64
|
+
### 2. Proposed Solution
|
|
65
|
+
<!-- Describe the solution you'd like. -->
|
|
66
|
+
|
|
67
|
+
### 3. Acceptance Criteria
|
|
68
|
+
- [ ] Functional requirements met
|
|
69
|
+
- [ ] Automated tests added
|
|
70
|
+
- [ ] Documentation updated
|
|
71
|
+
|
|
72
|
+
### 4. Alternatives Considered
|
|
73
|
+
<!-- Any alternatives or workaround you have considered. -->
|
|
74
|
+
`
|
|
75
|
+
|
|
76
|
+
templates['.github/ISSUE_TEMPLATE/security_advisory.md'] = `---
|
|
77
|
+
name: Security Vulnerability
|
|
78
|
+
about: Privately report a security issue
|
|
79
|
+
title: 'sec: '
|
|
80
|
+
labels: ['security']
|
|
81
|
+
assignees: ''
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
> [!IMPORTANT]
|
|
85
|
+
> If this is a critical zero-day security vulnerability, please contact ${contactEmail} directly instead of opening a public issue.
|
|
86
|
+
|
|
87
|
+
### 1. Summary
|
|
88
|
+
### 2. Severity & Impact
|
|
89
|
+
### 3. Steps / Proof of Concept
|
|
90
|
+
### 4. Remediation / Suggested Patch
|
|
91
|
+
`
|
|
92
|
+
|
|
93
|
+
templates['.github/PULL_REQUEST_TEMPLATE.md'] = `## 1. Context & Problem
|
|
94
|
+
<!-- Reference issue or explain the problem solved. -->
|
|
95
|
+
|
|
96
|
+
## 2. Proposed Changes
|
|
97
|
+
<!-- Detailed explanation of architectural and code modifications. -->
|
|
98
|
+
|
|
99
|
+
## 3. Verification & Testing
|
|
100
|
+
- [ ] Unit tests added / updated
|
|
101
|
+
- [ ] All tests passing (\`npm test\`)
|
|
102
|
+
- [ ] No regression on existing invariants
|
|
103
|
+
|
|
104
|
+
## 4. Documentation & Contracts
|
|
105
|
+
- [ ] Documentation updated (README / API docs)
|
|
106
|
+
- [ ] Conventional Commits used for automated release notes
|
|
107
|
+
|
|
108
|
+
## 5. Review Checklist
|
|
109
|
+
- [ ] Code style and lint verified
|
|
110
|
+
- [ ] Zero hardcoded secrets or credentials
|
|
111
|
+
`
|
|
112
|
+
|
|
113
|
+
templates['SECURITY.md'] = `# Security Policy
|
|
114
|
+
|
|
115
|
+
## Supported Versions
|
|
116
|
+
|
|
117
|
+
| Version | Supported |
|
|
118
|
+
| ------- | ------------------ |
|
|
119
|
+
| ${supportVersion} | :white_check_mark: |
|
|
120
|
+
| < 0.6.0 | :x: |
|
|
121
|
+
|
|
122
|
+
## Reporting a Vulnerability
|
|
123
|
+
|
|
124
|
+
Please report security vulnerabilities directly to \`${contactEmail}\`.
|
|
125
|
+
All reports will be acknowledged within 48 hours, and an advisory/patch roadmap provided within 7 days.
|
|
126
|
+
`
|
|
127
|
+
|
|
128
|
+
templates['CONTRIBUTING.md'] = `# Contributing to ${projectName}
|
|
129
|
+
|
|
130
|
+
Thank you for your interest in contributing!
|
|
131
|
+
|
|
132
|
+
## Development Workflow
|
|
133
|
+
1. Fork or branch from \`${defaultBranch}\`.
|
|
134
|
+
2. Install dependencies: \`pnpm install\` or \`npm install\`.
|
|
135
|
+
3. Run tests: \`npm test\`.
|
|
136
|
+
|
|
137
|
+
## Conventional Commits
|
|
138
|
+
We follow the Conventional Commits specification:
|
|
139
|
+
- \`feat:\` new feature (triggers minor version bump)
|
|
140
|
+
- \`fix:\` bug fix (triggers patch bump)
|
|
141
|
+
- \`perf:\` performance improvement
|
|
142
|
+
- \`refactor:\` code refactoring
|
|
143
|
+
- \`docs:\` documentation updates
|
|
144
|
+
- \`chore:\` maintenance and tooling
|
|
145
|
+
- \`BREAKING CHANGE:\` incompatible API change (triggers major bump)
|
|
146
|
+
|
|
147
|
+
## Pull Request Process
|
|
148
|
+
- Fill out the 5-section PR template.
|
|
149
|
+
- Ensure all automated checks pass.
|
|
150
|
+
`
|
|
151
|
+
|
|
152
|
+
templates['CODE_OF_CONDUCT.md'] = `# Contributor Covenant Code of Conduct
|
|
153
|
+
|
|
154
|
+
## Our Pledge
|
|
155
|
+
We as members, contributors, and leaders pledge to make participation in our
|
|
156
|
+
community a harassment-free experience for everyone, regardless of age, body
|
|
157
|
+
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
|
158
|
+
identity and expression, level of experience, education, socio-economic status,
|
|
159
|
+
nationality, personal appearance, race, caste, color, religion, or sexual
|
|
160
|
+
identity and orientation.
|
|
161
|
+
|
|
162
|
+
## Enforcement & Contact
|
|
163
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
164
|
+
reported to the community leadership at \`${contactEmail}\`.
|
|
165
|
+
`
|
|
166
|
+
|
|
167
|
+
return templates
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Map friendly template keys to path patterns */
|
|
171
|
+
const KEY_TO_PATH_PATTERN = {
|
|
172
|
+
bug_report: '.github/ISSUE_TEMPLATE/bug_report.md',
|
|
173
|
+
feature_request: '.github/ISSUE_TEMPLATE/feature_request.md',
|
|
174
|
+
security_advisory: '.github/ISSUE_TEMPLATE/security_advisory.md',
|
|
175
|
+
issue_templates: '.github/ISSUE_TEMPLATE/',
|
|
176
|
+
pull_request: '.github/PULL_REQUEST_TEMPLATE.md',
|
|
177
|
+
pr_template: '.github/PULL_REQUEST_TEMPLATE.md',
|
|
178
|
+
security: 'SECURITY.md',
|
|
179
|
+
contributing: 'CONTRIBUTING.md',
|
|
180
|
+
code_of_conduct: 'CODE_OF_CONDUCT.md',
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Scaffold community files into a repo branch or preview via dryRun. */
|
|
184
|
+
export async function scaffoldCommunityFiles(client, {
|
|
185
|
+
owner, repo, branch, templates = 'all', options = {}, dryRun = false, message,
|
|
186
|
+
} = {}) {
|
|
187
|
+
const allTemplates = getCommunityTemplates({
|
|
188
|
+
projectName: options.projectName || repo || 'Project',
|
|
189
|
+
contactEmail: options.contactEmail || 'security@goodandready.app',
|
|
190
|
+
defaultBranch: branch || 'main',
|
|
191
|
+
supportVersion: options.supportVersion || '>= 0.6.0',
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
let selectedPaths = Object.keys(allTemplates)
|
|
195
|
+
if (Array.isArray(templates) && !templates.includes('all')) {
|
|
196
|
+
const desiredPatterns = templates.map((t) => KEY_TO_PATH_PATTERN[t] || t)
|
|
197
|
+
selectedPaths = selectedPaths.filter((filePath) =>
|
|
198
|
+
desiredPatterns.some((pattern) => filePath === pattern || filePath.startsWith(pattern))
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const files = selectedPaths.map((filePath) => ({
|
|
203
|
+
path: filePath,
|
|
204
|
+
content: allTemplates[filePath],
|
|
205
|
+
}))
|
|
206
|
+
|
|
207
|
+
if (dryRun) {
|
|
208
|
+
return {
|
|
209
|
+
dryRun: true,
|
|
210
|
+
repository: `${owner}/${repo}`,
|
|
211
|
+
branch: branch || 'main',
|
|
212
|
+
count: files.length,
|
|
213
|
+
files,
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!client || !owner || !repo) {
|
|
218
|
+
throw new Error('client, owner and repo are required to commit files')
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const commitMessage = message || 'chore: scaffold GoodAndReady community health files'
|
|
222
|
+
const pushed = await pushFiles(client, {
|
|
223
|
+
owner,
|
|
224
|
+
repo,
|
|
225
|
+
branch: branch || 'main',
|
|
226
|
+
message: commitMessage,
|
|
227
|
+
files,
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
success: true,
|
|
232
|
+
repository: `${owner}/${repo}`,
|
|
233
|
+
branch: pushed.branch,
|
|
234
|
+
commit: pushed.commit,
|
|
235
|
+
count: files.length,
|
|
236
|
+
files: files.map((f) => f.path),
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -7,6 +7,7 @@ import { runReleasePreflight } from './release-preflight.js'
|
|
|
7
7
|
// and the definitions are reusable in tests.
|
|
8
8
|
|
|
9
9
|
import { listReleases, getRelease, createRelease, editRelease, deleteRelease, listReleaseAssets, uploadReleaseAsset, deleteReleaseAsset, generateReleaseNotes } from './releases.js'
|
|
10
|
+
import { generateChangelog } from './changelog.js'
|
|
10
11
|
import { listTags, createTag, deleteTag } from './tags.js'
|
|
11
12
|
import { callApi } from './api.js'
|
|
12
13
|
import { getRepo, getFile, searchRepos, createRepo, editRepo } from './repos.js'
|
|
@@ -264,6 +265,31 @@ export function registerCoreTools({ tool, asRepo, client, liveConfig, defaultCwd
|
|
|
264
265
|
},
|
|
265
266
|
)
|
|
266
267
|
|
|
268
|
+
|
|
269
|
+
// ------------------------------------------------------------------------ conventional changelog (#123)
|
|
270
|
+
|
|
271
|
+
tool(
|
|
272
|
+
'gh_changelog_generate',
|
|
273
|
+
'Generate structured Conventional Commits changelog, group by category, extract breaking changes, and recommend SemVer bump (major/minor/patch). Read-only.',
|
|
274
|
+
repoParams({
|
|
275
|
+
base: { type: 'string', description: 'Base tag, branch or commit SHA to compare from. If omitted, uses the latest tag or initial commit.' },
|
|
276
|
+
head: { type: 'string', description: 'Head tag, branch or commit SHA to compare to (default "HEAD").' },
|
|
277
|
+
version: { type: 'string', description: 'Version label for the changelog entry (optional, defaults to calculated next version).' },
|
|
278
|
+
date: { type: 'string', description: 'Date for the changelog header (defaults to current YYYY-MM-DD).' },
|
|
279
|
+
}),
|
|
280
|
+
async (args) => {
|
|
281
|
+
const { owner, repo } = await asRepo(args, liveConfig())
|
|
282
|
+
return generateChangelog(await client(), {
|
|
283
|
+
owner,
|
|
284
|
+
repo,
|
|
285
|
+
base: args.base,
|
|
286
|
+
head: args.head,
|
|
287
|
+
version: args.version,
|
|
288
|
+
date: args.date,
|
|
289
|
+
})
|
|
290
|
+
},
|
|
291
|
+
)
|
|
292
|
+
|
|
267
293
|
// ------------------------------------------------------------------------ tags
|
|
268
294
|
|
|
269
295
|
tool(
|
|
@@ -91,6 +91,7 @@ export function registerWriteTools({ tool, asRepo, client, liveConfig, auth }) {
|
|
|
91
91
|
title: { type: 'string', description: 'Commit message and pull request title.' },
|
|
92
92
|
body: { type: 'string', description: 'Pull request body.' },
|
|
93
93
|
createPullRequest: { type: 'boolean', description: 'Open a pull request after publishing.' },
|
|
94
|
+
includeCommunityFiles: { type: 'boolean', description: 'Automatically include standardized GoodAndReady community files (issue templates, PR template, SECURITY.md, CONTRIBUTING.md, CODE_OF_CONDUCT.md).' },
|
|
94
95
|
},
|
|
95
96
|
async (args) => {
|
|
96
97
|
const cfg = liveConfig()
|
|
@@ -116,6 +117,7 @@ export function registerWriteTools({ tool, asRepo, client, liveConfig, auth }) {
|
|
|
116
117
|
title: args.title,
|
|
117
118
|
body: args.body,
|
|
118
119
|
createPullRequest: args.createPullRequest === true,
|
|
120
|
+
includeCommunityFiles: args.includeCommunityFiles === true,
|
|
119
121
|
})
|
|
120
122
|
},
|
|
121
123
|
)
|
|
@@ -150,6 +152,41 @@ export function registerWriteTools({ tool, asRepo, client, liveConfig, auth }) {
|
|
|
150
152
|
},
|
|
151
153
|
)
|
|
152
154
|
|
|
155
|
+
|
|
156
|
+
// ------------------------------------------------------------------------ community scaffold (#126)
|
|
157
|
+
|
|
158
|
+
tool(
|
|
159
|
+
'gh_community_scaffold',
|
|
160
|
+
'Scaffold standardized GoodAndReady community health files (issue templates, PR template, SECURITY.md, CONTRIBUTING.md, CODE_OF_CONDUCT.md). Writes files or previews with dryRun.',
|
|
161
|
+
repoParams({
|
|
162
|
+
branch: { type: 'string', description: 'Target branch to commit files to (defaults to repository default branch).' },
|
|
163
|
+
templates: {
|
|
164
|
+
type: 'array',
|
|
165
|
+
items: { type: 'string' },
|
|
166
|
+
description: 'Templates to generate: ["all"] or subset of ["bug_report", "feature_request", "security_advisory", "pull_request", "security", "contributing", "code_of_conduct"]. Default "all".',
|
|
167
|
+
},
|
|
168
|
+
projectName: { type: 'string', description: 'Human-readable project name.' },
|
|
169
|
+
contactEmail: { type: 'string', description: 'Security and contact email address.' },
|
|
170
|
+
dryRun: { type: 'boolean', description: 'If true, returns file contents without creating a commit. Default false.' },
|
|
171
|
+
message: { type: 'string', description: 'Commit message for scaffolding commit.' },
|
|
172
|
+
}),
|
|
173
|
+
async (args) => {
|
|
174
|
+
const { owner, repo } = await asRepo(args, liveConfig())
|
|
175
|
+
return scaffoldCommunityFiles(await client(), {
|
|
176
|
+
owner,
|
|
177
|
+
repo,
|
|
178
|
+
branch: args.branch,
|
|
179
|
+
templates: args.templates || 'all',
|
|
180
|
+
options: {
|
|
181
|
+
projectName: args.projectName,
|
|
182
|
+
contactEmail: args.contactEmail,
|
|
183
|
+
},
|
|
184
|
+
dryRun: Boolean(args.dryRun),
|
|
185
|
+
message: args.message,
|
|
186
|
+
})
|
|
187
|
+
},
|
|
188
|
+
)
|
|
189
|
+
|
|
153
190
|
// ------------------------------------------------------------------ account
|
|
154
191
|
|
|
155
192
|
tool(
|
package/lib/tools/write-files.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { encodeRepoPath } from '../github.js'
|
|
2
|
+
import { getCommunityTemplates } from './community.js'
|
|
2
3
|
// Writing files without a local checkout.
|
|
3
4
|
//
|
|
4
5
|
// A commit is built the way git builds it: blobs, a tree on top of the branch's tree, a
|
|
@@ -139,9 +140,20 @@ function defaultSleep(ms) {
|
|
|
139
140
|
export async function uploadProject(client, {
|
|
140
141
|
owner, repo, description, private: isPrivate = true, files = [], branch, base, title, body,
|
|
141
142
|
createPullRequest = false, org, attempts = 5, sleep = defaultSleep,
|
|
143
|
+
includeCommunityFiles = false, communityOptions = {},
|
|
142
144
|
} = {}) {
|
|
143
145
|
if (!repo) throw new Error('repository name is required')
|
|
144
|
-
|
|
146
|
+
let filesToPush = [...(Array.isArray(files) ? files : [])]
|
|
147
|
+
if (includeCommunityFiles) {
|
|
148
|
+
const templates = getCommunityTemplates({ projectName: repo, ...communityOptions })
|
|
149
|
+
const existing = new Set(filesToPush.map((f) => String(f.path || '').replace(/^\/+/g, '')))
|
|
150
|
+
for (const [tplPath, tplContent] of Object.entries(templates)) {
|
|
151
|
+
if (!existing.has(tplPath)) {
|
|
152
|
+
filesToPush.push({ path: tplPath, content: tplContent })
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (filesToPush.length === 0) throw new Error('files must be a non-empty array')
|
|
145
157
|
|
|
146
158
|
let repository = null
|
|
147
159
|
try {
|
|
@@ -164,7 +176,7 @@ export async function uploadProject(client, {
|
|
|
164
176
|
const target = branch || (await repoDefaultBranch(client, { owner: repoOwner, repo: repoName, attempts, sleep }))
|
|
165
177
|
|
|
166
178
|
const pushed = await pushFiles(client, {
|
|
167
|
-
owner: repoOwner, repo: repoName, branch: target, message: title || 'Initial commit', files, base,
|
|
179
|
+
owner: repoOwner, repo: repoName, branch: target, message: title || 'Initial commit', files: filesToPush, base,
|
|
168
180
|
})
|
|
169
181
|
|
|
170
182
|
let pullRequest = null
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-github-ops",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.9",
|
|
4
4
|
"description": "GitHub operations for DeepSeek Harness: releases, tags, a guarded generic API pass-through, workflow runs, secrets and sanitized mirror publication.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|