@goodandready/dsh-github-ops 0.2.6 → 0.2.7
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 +18 -0
- package/lib/routes.js +4 -2
- package/lib/scm-write.js +0 -0
- package/lib/scm.js +38 -1
- package/lib/tools/register-core.js +18 -0
- package/lib/tools/release-preflight.js +202 -0
- package/lib/tools/releases.js +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Notable changes to `@goodandready/dsh-github-ops`.
|
|
4
4
|
|
|
5
|
+
## 0.2.7
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Git Worktrees Support** (#47): porcelain worktrees inventory parsing (`parseWorktrees`), `worktreeAdd` and `worktreeRemove` actions with confirm protection, and UI worktree switching bar in Source Control tab.
|
|
9
|
+
- **Automated Release Preflight** (#65): `gh_release_preflight` tool validating package version, cordis.patch.yml alignment, changelog release sections, 3-language READMEs (EN, RU, ZH), package bundle size limits (<=256 KiB), and git index hygiene.
|
|
10
|
+
|
|
5
11
|
## 0.2.6
|
|
6
12
|
|
|
7
13
|
### Fixed
|
package/lib/client.js
CHANGED
|
@@ -104,6 +104,7 @@ window.__ModuleLoader__.load({
|
|
|
104
104
|
scmNoChanges: 'Nothing staged or changed',
|
|
105
105
|
scmBehindHint: 'Behind the upstream: sync pulls with rebase, then pushes',
|
|
106
106
|
scmPushBranch: 'Push branch',
|
|
107
|
+
scmWorktrees: 'Worktrees',
|
|
107
108
|
scmRepoPath: 'Repository path (optional)',
|
|
108
109
|
scmRepoPathHint: 'Absolute path; empty means the session workspace',
|
|
109
110
|
prBarBranch: 'to',
|
|
@@ -205,6 +206,7 @@ window.__ModuleLoader__.load({
|
|
|
205
206
|
scmNoChanges: '没有已暂存或已修改的内容',
|
|
206
207
|
scmBehindHint: '落后于上游:同步会先 rebase 拉取,再推送',
|
|
207
208
|
scmPushBranch: '推送分支',
|
|
209
|
+
scmWorktrees: '工作树',
|
|
208
210
|
scmRepoPath: '仓库路径(可选)',
|
|
209
211
|
scmRepoPathHint: '绝对路径;留空则使用会话工作区',
|
|
210
212
|
prBarBranch: '→',
|
|
@@ -428,6 +430,21 @@ window.__ModuleLoader__.load({
|
|
|
428
430
|
)),
|
|
429
431
|
)
|
|
430
432
|
|
|
433
|
+
const worktreeBar = (payload.worktrees && payload.worktrees.length > 0)
|
|
434
|
+
? React.createElement('div', { className: 'gho-scm-bar' },
|
|
435
|
+
React.createElement('span', { className: 'gho-scm-counts' }, t('scmWorktrees')),
|
|
436
|
+
payload.worktrees.map((wt) => {
|
|
437
|
+
const label = wt.branch ? `${wt.branch} (${wt.path.split('/').pop()})` : (wt.path.split('/').pop() || wt.path)
|
|
438
|
+
const isCurrent = wt.path === payload.cwd
|
|
439
|
+
return button(
|
|
440
|
+
isCurrent ? `● ${label}` : label,
|
|
441
|
+
() => { if (!isCurrent) rememberPath(wt.path) },
|
|
442
|
+
{ disabled: isCurrent || Boolean(busy) },
|
|
443
|
+
)
|
|
444
|
+
}),
|
|
445
|
+
)
|
|
446
|
+
: null
|
|
447
|
+
|
|
431
448
|
const stashBar = React.createElement('div', { className: 'gho-scm-bar' },
|
|
432
449
|
React.createElement('input', {
|
|
433
450
|
type: 'text',
|
|
@@ -479,6 +496,7 @@ window.__ModuleLoader__.load({
|
|
|
479
496
|
: null,
|
|
480
497
|
branchBar,
|
|
481
498
|
stashBar,
|
|
499
|
+
worktreeBar,
|
|
482
500
|
React.createElement('div', { className: 'gho-scm-meta' },
|
|
483
501
|
React.createElement('span', null, `${t('scmTags')}: ${(payload.tags || []).join(', ') || '—'}`),
|
|
484
502
|
),
|
package/lib/routes.js
CHANGED
|
@@ -10,7 +10,7 @@ import { join as joinPath } from 'node:path'
|
|
|
10
10
|
|
|
11
11
|
import { statusPayload, isTrustedRequest, sendJson } from './status.js'
|
|
12
12
|
import { buildAction, validateCwd } from './scm-write.js'
|
|
13
|
-
import { parseStatus, parseBranches, parseStashes, parseCount, scmPayload, mergeStateFrom } from './scm.js'
|
|
13
|
+
import { parseStatus, parseBranches, parseStashes, parseWorktrees, parseCount, scmPayload, mergeStateFrom } from './scm.js'
|
|
14
14
|
import { listIssues } from './tools/issues.js'
|
|
15
15
|
import { listRuns } from './tools/actions.js'
|
|
16
16
|
import { listNotifications, repoTreeView } from './tools/panel-data.js'
|
|
@@ -179,11 +179,12 @@ export function registerRoutes(ctx, deps = {}) {
|
|
|
179
179
|
ahead = aheadAnswer.code === 0 ? parseCount(aheadAnswer.stdout) : null
|
|
180
180
|
behind = behindAnswer.code === 0 ? parseCount(behindAnswer.stdout) : null
|
|
181
181
|
}
|
|
182
|
-
const [statusAnswer, branchesAnswer, tagsAnswer, stashesAnswer] = await Promise.all([
|
|
182
|
+
const [statusAnswer, branchesAnswer, tagsAnswer, stashesAnswer, worktreesAnswer] = await Promise.all([
|
|
183
183
|
git(['status', '--porcelain=v1'], { cwd }),
|
|
184
184
|
git(['branch', '--format=%(refname:short)\t%(HEAD)'], { cwd }),
|
|
185
185
|
git(['tag', '--list'], { cwd }),
|
|
186
186
|
git(['stash', 'list', '--format=%gd\t%gs'], { cwd }),
|
|
187
|
+
git(['worktree', 'list', '--porcelain'], { cwd }),
|
|
187
188
|
])
|
|
188
189
|
const gitDirAnswer = await git(['rev-parse', '--absolute-git-dir'], { cwd })
|
|
189
190
|
const gitDir = gitDirAnswer.code === 0 ? gitDirAnswer.stdout.trim() : joinPath(cwd, '.git')
|
|
@@ -203,6 +204,7 @@ export function registerRoutes(ctx, deps = {}) {
|
|
|
203
204
|
branches: parseBranches(branchesAnswer.code === 0 ? branchesAnswer.stdout : ''),
|
|
204
205
|
tags: tagsAnswer.code === 0 ? tagsAnswer.stdout.split('\n').map((t) => t.trim()).filter(Boolean).slice(0, 50) : [],
|
|
205
206
|
stashes: parseStashes(stashesAnswer.code === 0 ? stashesAnswer.stdout : ''),
|
|
207
|
+
worktrees: parseWorktrees(worktreesAnswer.code === 0 ? worktreesAnswer.stdout : ''),
|
|
206
208
|
merge,
|
|
207
209
|
}))
|
|
208
210
|
},
|
package/lib/scm-write.js
CHANGED
|
Binary file
|
package/lib/scm.js
CHANGED
|
@@ -76,6 +76,42 @@ export function parseStashes(text) {
|
|
|
76
76
|
})
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/** Parse output of `git worktree list --porcelain`. */
|
|
80
|
+
export function parseWorktrees(text) {
|
|
81
|
+
const chunks = String(text || '').trim().split(/\n\s*\n/)
|
|
82
|
+
const worktrees = []
|
|
83
|
+
for (const chunk of chunks) {
|
|
84
|
+
if (!chunk.trim()) continue
|
|
85
|
+
const lines = chunk.split('\n')
|
|
86
|
+
let path = ''
|
|
87
|
+
let head = ''
|
|
88
|
+
let branch = ''
|
|
89
|
+
let bare = false
|
|
90
|
+
let detached = false
|
|
91
|
+
let locked = false
|
|
92
|
+
let lockReason = ''
|
|
93
|
+
let prunable = false
|
|
94
|
+
|
|
95
|
+
for (const line of lines) {
|
|
96
|
+
if (line.startsWith('worktree ')) path = line.slice(9).trim()
|
|
97
|
+
else if (line.startsWith('HEAD ')) head = line.slice(5).trim()
|
|
98
|
+
else if (line.startsWith('branch ')) branch = line.slice(7).trim().replace(/^refs\/heads\//, '')
|
|
99
|
+
else if (line === 'bare') bare = true
|
|
100
|
+
else if (line === 'detached') detached = true
|
|
101
|
+
else if (line.startsWith('locked')) {
|
|
102
|
+
locked = true
|
|
103
|
+
lockReason = line.slice(6).trim()
|
|
104
|
+
} else if (line.startsWith('prunable')) {
|
|
105
|
+
prunable = true
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (path) {
|
|
109
|
+
worktrees.push({ path, head, branch, bare, detached, locked, lockReason: lockReason || undefined, prunable })
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return worktrees
|
|
113
|
+
}
|
|
114
|
+
|
|
79
115
|
/** Counts from `git rev-list --count <range>`. */
|
|
80
116
|
export function parseCount(text) {
|
|
81
117
|
const value = Number.parseInt(String(text || '').trim(), 10)
|
|
@@ -87,7 +123,7 @@ export function parseCount(text) {
|
|
|
87
123
|
* ahead/behind, and that is reported as such rather than as an error.
|
|
88
124
|
*/
|
|
89
125
|
export function scmPayload({
|
|
90
|
-
cwd, branch, upstream, ahead, behind, status, branches, tags, stashes, merge, error,
|
|
126
|
+
cwd, branch, upstream, ahead, behind, status, branches, tags, stashes, worktrees, merge, error,
|
|
91
127
|
} = {}) {
|
|
92
128
|
return {
|
|
93
129
|
cwd: cwd || '',
|
|
@@ -100,6 +136,7 @@ export function scmPayload({
|
|
|
100
136
|
branches: Array.isArray(branches) ? branches : [],
|
|
101
137
|
tags: Array.isArray(tags) ? tags : [],
|
|
102
138
|
stashes: Array.isArray(stashes) ? stashes : [],
|
|
139
|
+
worktrees: Array.isArray(worktrees) ? worktrees : [],
|
|
103
140
|
mergeState: merge || { inProgress: false, kind: '' },
|
|
104
141
|
error: error ? String(error).slice(0, 300) : '',
|
|
105
142
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { runReleasePreflight } from './release-preflight.js'
|
|
1
2
|
// Core tool registrations: repository and file reads, releases, tags, the guarded
|
|
2
3
|
// generic API pass-through, and repository creation/edits.
|
|
3
4
|
//
|
|
@@ -332,4 +333,21 @@ export function registerCoreTools({ tool, asRepo, client, liveConfig }) {
|
|
|
332
333
|
}
|
|
333
334
|
},
|
|
334
335
|
)
|
|
336
|
+
|
|
337
|
+
tool(
|
|
338
|
+
'gh_release_preflight',
|
|
339
|
+
'Run automated pre-release audit on the plugin package: checks package.json version, cordis.patch.yml, CHANGELOG.md release notes, three READMEs (EN/RU/ZH), npm pack file sizes (<256 KiB), and git index hygiene.',
|
|
340
|
+
{
|
|
341
|
+
cwd: { type: 'string', description: 'Directory to audit (defaults to workspace or process.cwd()).' },
|
|
342
|
+
runTests: { type: 'boolean', description: 'Whether to run npm test as part of the audit (default false).' },
|
|
343
|
+
maxFileSize: { type: 'number', description: 'Max allowed size in bytes per packaged file (default 262144).' },
|
|
344
|
+
},
|
|
345
|
+
async (args) => {
|
|
346
|
+
return runReleasePreflight({
|
|
347
|
+
cwd: args.cwd,
|
|
348
|
+
runTests: args.runTests,
|
|
349
|
+
maxFileSize: args.maxFileSize,
|
|
350
|
+
})
|
|
351
|
+
},
|
|
352
|
+
)
|
|
335
353
|
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// lib/tools/release-preflight.js
|
|
2
|
+
// Automated release preflight audit for DSH plugins according to
|
|
3
|
+
// dhs-plugin-release-workflow and dsh-documentation-standard.
|
|
4
|
+
|
|
5
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
6
|
+
import { join as joinPath } from 'node:path'
|
|
7
|
+
import { execFile as defaultExecFile } from 'node:child_process'
|
|
8
|
+
import { promisify } from 'node:util'
|
|
9
|
+
|
|
10
|
+
const pExecFile = promisify(defaultExecFile)
|
|
11
|
+
|
|
12
|
+
const MAX_FILE_SIZE_DEFAULT = 262144 // 256 KiB per file
|
|
13
|
+
const FORBIDDEN_PACK_PREFIXES = ['docs/', 'test/', 'tests/', '.planning/', '.worktrees/']
|
|
14
|
+
const FORBIDDEN_PACK_EXACT = ['AGENTS.md', '.env', '.env.local', 'deploy.sh']
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Runs preflight release audit checks on a plugin directory.
|
|
18
|
+
*
|
|
19
|
+
* @param {object} options
|
|
20
|
+
* @param {string} [options.cwd] Directory to check (default: process.cwd())
|
|
21
|
+
* @param {boolean} [options.runTests] Whether to execute npm test (default: false)
|
|
22
|
+
* @param {number} [options.maxFileSize] Max allowed size for any single packaged file (default: 256 KiB)
|
|
23
|
+
* @param {object} [deps] Injectable dependencies for unit testing
|
|
24
|
+
*/
|
|
25
|
+
export async function runReleasePreflight(options = {}, deps = {}) {
|
|
26
|
+
const cwd = options.cwd || process.cwd()
|
|
27
|
+
const runTests = options.runTests === true
|
|
28
|
+
const maxFileSize = typeof options.maxFileSize === 'number' ? options.maxFileSize : MAX_FILE_SIZE_DEFAULT
|
|
29
|
+
const exec = deps.execFile || pExecFile
|
|
30
|
+
const fsRead = deps.readFileSync || readFileSync
|
|
31
|
+
const fsExists = deps.existsSync || existsSync
|
|
32
|
+
|
|
33
|
+
const checks = []
|
|
34
|
+
const findings = []
|
|
35
|
+
|
|
36
|
+
// Helper to record result
|
|
37
|
+
const record = (name, ok, details, isWarn = false) => {
|
|
38
|
+
checks.push({ name, ok, details, severity: ok ? 'info' : (isWarn ? 'warn' : 'fail') })
|
|
39
|
+
if (!ok && !isWarn) {
|
|
40
|
+
findings.push(`[FAIL] ${name}: ${details}`)
|
|
41
|
+
} else if (!ok && isWarn) {
|
|
42
|
+
findings.push(`[WARN] ${name}: ${details}`)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 1. Check package.json
|
|
47
|
+
let pkg = null
|
|
48
|
+
const pkgPath = joinPath(cwd, 'package.json')
|
|
49
|
+
if (!fsExists(pkgPath)) {
|
|
50
|
+
record('package.json', false, 'package.json not found in repository root')
|
|
51
|
+
return { ok: false, verdict: 'FAIL', checks, findings, version: '' }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
pkg = JSON.parse(fsRead(pkgPath, 'utf8'))
|
|
56
|
+
const hasName = typeof pkg.name === 'string' && pkg.name.trim().length > 0
|
|
57
|
+
const hasVersion = typeof pkg.version === 'string' && /^\d+\.\d+\.\d+/.test(pkg.version)
|
|
58
|
+
record('package.json', hasName && hasVersion, `name="${pkg.name}", version="${pkg.version}"`)
|
|
59
|
+
} catch (err) {
|
|
60
|
+
record('package.json', false, `invalid JSON: ${err.message}`)
|
|
61
|
+
return { ok: false, verdict: 'FAIL', checks, findings, version: '' }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const version = pkg.version || ''
|
|
65
|
+
const pkgName = pkg.name || ''
|
|
66
|
+
|
|
67
|
+
// 2. Check cordis.patch.yml matching package.json
|
|
68
|
+
const patchPath = joinPath(cwd, 'cordis.patch.yml')
|
|
69
|
+
if (fsExists(patchPath)) {
|
|
70
|
+
try {
|
|
71
|
+
const patchText = fsRead(patchPath, 'utf8')
|
|
72
|
+
const matchesName = patchText.includes(pkgName)
|
|
73
|
+
record('cordis.patch.yml', matchesName, matchesName ? `matches package name "${pkgName}"` : `does not contain package name "${pkgName}"`)
|
|
74
|
+
} catch (err) {
|
|
75
|
+
record('cordis.patch.yml', false, `read error: ${err.message}`)
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
record('cordis.patch.yml', false, 'cordis.patch.yml not found', true)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 3. Check CHANGELOG.md contains release notes for version
|
|
82
|
+
const changelogPath = joinPath(cwd, 'CHANGELOG.md')
|
|
83
|
+
if (fsExists(changelogPath)) {
|
|
84
|
+
try {
|
|
85
|
+
const clText = fsRead(changelogPath, 'utf8')
|
|
86
|
+
const versionRe = new RegExp(`(^|\\n)##+\\s+(\\[?v?${version.replace(/\./g, '\\.')}\\]?)`, 'i')
|
|
87
|
+
const hasNotes = versionRe.test(clText) || clText.includes(version)
|
|
88
|
+
record('CHANGELOG.md', hasNotes, hasNotes ? `found release section for ${version}` : `missing release entry for ${version}`)
|
|
89
|
+
} catch (err) {
|
|
90
|
+
record('CHANGELOG.md', false, `read error: ${err.message}`)
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
record('CHANGELOG.md', false, 'CHANGELOG.md not found')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 4. Check tri-lingual READMEs
|
|
97
|
+
const readmes = [
|
|
98
|
+
{ file: 'README.md', lang: 'EN' },
|
|
99
|
+
{ file: 'README.ru.md', lang: 'RU' },
|
|
100
|
+
{ file: 'README.zh.md', lang: 'ZH' },
|
|
101
|
+
]
|
|
102
|
+
for (const item of readmes) {
|
|
103
|
+
const fPath = joinPath(cwd, item.file)
|
|
104
|
+
if (fsExists(fPath)) {
|
|
105
|
+
try {
|
|
106
|
+
const content = fsRead(fPath, 'utf8')
|
|
107
|
+
const okLen = content.trim().length >= 20 || Buffer.byteLength(content) >= 50
|
|
108
|
+
record(`README (${item.lang})`, okLen, okLen ? `${item.file} present (${content.length} bytes)` : `${item.file} is suspiciously short`)
|
|
109
|
+
} catch (err) {
|
|
110
|
+
record(`README (${item.lang})`, false, `read error: ${err.message}`)
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
record(`README (${item.lang})`, false, `${item.file} missing`)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 5. Check npm pack --dry-run --json composition and sizes
|
|
118
|
+
let packFiles = []
|
|
119
|
+
let packedBytes = 0
|
|
120
|
+
let unpackedBytes = 0
|
|
121
|
+
try {
|
|
122
|
+
const { stdout } = await exec('npm', ['pack', '--dry-run', '--json'], { cwd, maxBuffer: 10 * 1024 * 1024 })
|
|
123
|
+
const jsonStart = stdout.search(/[{\[]/)
|
|
124
|
+
if (jsonStart !== -1) {
|
|
125
|
+
const raw = JSON.parse(stdout.slice(jsonStart))
|
|
126
|
+
const packData = Array.isArray(raw) ? raw[0] : (Object.values(raw)[0] || raw)
|
|
127
|
+
packFiles = Array.isArray(packData.files) ? packData.files : []
|
|
128
|
+
packedBytes = packData.size || 0
|
|
129
|
+
unpackedBytes = packData.unpackedSize || 0
|
|
130
|
+
}
|
|
131
|
+
} catch (err) {
|
|
132
|
+
record('npm pack', false, `npm pack --dry-run failed: ${err.message}`)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (packFiles.length > 0) {
|
|
136
|
+
const oversized = packFiles.filter((f) => (f.size || 0) > maxFileSize)
|
|
137
|
+
record(
|
|
138
|
+
'npm pack file sizes',
|
|
139
|
+
oversized.length === 0,
|
|
140
|
+
oversized.length === 0
|
|
141
|
+
? `all ${packFiles.length} files <= ${maxFileSize} bytes`
|
|
142
|
+
: `oversized files: ${oversized.map((f) => `${f.path} (${f.size}B)`).join(', ')}`
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
const leakedFiles = packFiles.filter((f) => {
|
|
146
|
+
const p = f.path || ''
|
|
147
|
+
if (FORBIDDEN_PACK_EXACT.includes(p)) return true
|
|
148
|
+
return FORBIDDEN_PACK_PREFIXES.some((prefix) => p.startsWith(prefix))
|
|
149
|
+
})
|
|
150
|
+
record(
|
|
151
|
+
'npm pack hygiene',
|
|
152
|
+
leakedFiles.length === 0,
|
|
153
|
+
leakedFiles.length === 0
|
|
154
|
+
? 'no internal/forbidden docs or test files in package'
|
|
155
|
+
: `forbidden files in npm package: ${leakedFiles.map((f) => f.path).join(', ')}`
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 6. Check git tracking hygiene if in git repo
|
|
160
|
+
try {
|
|
161
|
+
const { stdout } = await exec('git', ['ls-files'], { cwd })
|
|
162
|
+
const tracked = stdout.split('\n').map((l) => l.trim()).filter(Boolean)
|
|
163
|
+
const trackedForbidden = tracked.filter((p) => {
|
|
164
|
+
return p === 'AGENTS.md' || p.startsWith('.planning') || p.startsWith('.worktrees') || p.startsWith('openwiki')
|
|
165
|
+
})
|
|
166
|
+
record(
|
|
167
|
+
'git tracked hygiene',
|
|
168
|
+
trackedForbidden.length === 0,
|
|
169
|
+
trackedForbidden.length === 0
|
|
170
|
+
? 'clean git index (no AGENTS.md, .planning, .worktrees)'
|
|
171
|
+
: `forbidden files tracked in git: ${trackedForbidden.join(', ')}`,
|
|
172
|
+
true // warn
|
|
173
|
+
)
|
|
174
|
+
} catch {
|
|
175
|
+
// not a git repo or git not found
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 7. Optional tests execution
|
|
179
|
+
if (runTests) {
|
|
180
|
+
try {
|
|
181
|
+
await exec('npm', ['test'], { cwd, timeout: 60000 })
|
|
182
|
+
record('npm test', true, 'test suite passed successfully')
|
|
183
|
+
} catch (err) {
|
|
184
|
+
record('npm test', false, `test suite failed: ${err.message}`)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const failures = checks.filter((c) => c.severity === 'fail')
|
|
189
|
+
const ok = failures.length === 0
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
ok,
|
|
193
|
+
verdict: ok ? 'PASS' : 'FAIL',
|
|
194
|
+
version,
|
|
195
|
+
packageName: pkgName,
|
|
196
|
+
packedBytes,
|
|
197
|
+
unpackedBytes,
|
|
198
|
+
unpackedSizeKiB: Math.round(unpackedBytes / 1024 * 10) / 10,
|
|
199
|
+
checks,
|
|
200
|
+
findings,
|
|
201
|
+
}
|
|
202
|
+
}
|
package/lib/tools/releases.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-github-ops",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
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",
|