@goodandready/dsh-github-ops 0.1.0
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 +81 -0
- package/LICENSE +21 -0
- package/README.md +163 -0
- package/README.ru.md +161 -0
- package/README.zh.md +151 -0
- package/cordis.patch.yml +3 -0
- package/lib/approval.js +168 -0
- package/lib/auth.js +59 -0
- package/lib/client.js +304 -0
- package/lib/commands.js +151 -0
- package/lib/github.js +315 -0
- package/lib/index.js +332 -0
- package/lib/jobs.js +107 -0
- package/lib/present.js +214 -0
- package/lib/repo-context.js +72 -0
- package/lib/review-rules.js +147 -0
- package/lib/tools/actions.js +107 -0
- package/lib/tools/api.js +44 -0
- package/lib/tools/issues.js +101 -0
- package/lib/tools/mirror.js +163 -0
- package/lib/tools/pulls.js +207 -0
- package/lib/tools/register-collab.js +233 -0
- package/lib/tools/register-core.js +259 -0
- package/lib/tools/register-jobs.js +57 -0
- package/lib/tools/register-mirror.js +88 -0
- package/lib/tools/register-ops.js +283 -0
- package/lib/tools/releases.js +111 -0
- package/lib/tools/repo-settings.js +207 -0
- package/lib/tools/repos.js +108 -0
- package/lib/tools/tags.js +68 -0
- package/package.json +73 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// Core tool registrations: repository and file reads, releases, tags, the guarded
|
|
2
|
+
// generic API pass-through, and repository creation/edits.
|
|
3
|
+
//
|
|
4
|
+
// Everything here takes its dependencies as arguments, so the host half stays thin
|
|
5
|
+
// and the definitions are reusable in tests.
|
|
6
|
+
|
|
7
|
+
import { listReleases, getRelease, createRelease, editRelease, deleteRelease } from './releases.js'
|
|
8
|
+
import { listTags, createTag, deleteTag } from './tags.js'
|
|
9
|
+
import { callApi } from './api.js'
|
|
10
|
+
import { getRepo, getFile, searchRepos, createRepo, editRepo } from './repos.js'
|
|
11
|
+
|
|
12
|
+
const REPOSITORY = {
|
|
13
|
+
repository: { type: 'string', description: 'Repository as "owner/repo". Omit to use the default from settings.' },
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function registerCoreTools({ tool, asRepo, client, liveConfig }) {
|
|
17
|
+
const repoParams = (extra = {}) => ({ ...REPOSITORY, ...extra })
|
|
18
|
+
|
|
19
|
+
// ------------------------------------------------------------------ repository
|
|
20
|
+
|
|
21
|
+
tool(
|
|
22
|
+
'gh_repo',
|
|
23
|
+
'Read GitHub repository metadata: description, default branch, stars, topics, license, open issue count. Read-only.',
|
|
24
|
+
repoParams(),
|
|
25
|
+
async (args) => {
|
|
26
|
+
const { owner, repo } = await asRepo(args, liveConfig())
|
|
27
|
+
return getRepo(await client(), { owner, repo })
|
|
28
|
+
},
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
tool(
|
|
32
|
+
'gh_file',
|
|
33
|
+
'Read one file from a GitHub repository at a branch, tag or commit (content is decoded for you). A directory path returns its entries. Read-only.',
|
|
34
|
+
repoParams({
|
|
35
|
+
path: { type: 'string', description: 'File path inside the repository, e.g. "lib/index.js" or "lib".' },
|
|
36
|
+
ref: { type: 'string', description: 'Branch, tag or commit; defaults to the default branch.' },
|
|
37
|
+
}),
|
|
38
|
+
async (args) => {
|
|
39
|
+
const { owner, repo } = await asRepo(args, liveConfig())
|
|
40
|
+
return getFile(await client(), { owner, repo, path: args.path, ref: args.ref })
|
|
41
|
+
},
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
tool(
|
|
45
|
+
'gh_repo_search',
|
|
46
|
+
'Search GitHub repositories by name, description or README (GitHub search syntax, e.g. "dsh-plugin in:name,description"). Uses the separate search quota. Read-only.',
|
|
47
|
+
{
|
|
48
|
+
query: { type: 'string', description: 'Search query in GitHub search syntax.' },
|
|
49
|
+
limit: { type: 'number', description: 'How many repositories to return (max 100, default 20).' },
|
|
50
|
+
},
|
|
51
|
+
async (args) => searchRepos(await client(), { query: args.query, limit: args.limit }),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
tool(
|
|
55
|
+
'gh_repo_create',
|
|
56
|
+
'Create a GitHub repository for the authenticated user, or inside an organization when org is given. Defaults to private.',
|
|
57
|
+
{
|
|
58
|
+
name: { type: 'string', description: 'Repository name.' },
|
|
59
|
+
org: { type: 'string', description: 'Organization to create it in; omit for the authenticated user.' },
|
|
60
|
+
description: { type: 'string', description: 'Repository description.' },
|
|
61
|
+
private: { type: 'boolean', description: 'Private repository (default true).' },
|
|
62
|
+
autoInit: { type: 'boolean', description: 'Initialize with a README (default false).' },
|
|
63
|
+
},
|
|
64
|
+
async (args) => createRepo(await client(), {
|
|
65
|
+
name: args.name,
|
|
66
|
+
org: args.org,
|
|
67
|
+
description: args.description,
|
|
68
|
+
private: args.private,
|
|
69
|
+
autoInit: args.autoInit,
|
|
70
|
+
}),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
tool(
|
|
74
|
+
'gh_repo_edit',
|
|
75
|
+
'Edit repository settings: description, homepage, visibility, archived flag and topics (topics replace the whole list).',
|
|
76
|
+
repoParams({
|
|
77
|
+
description: { type: 'string', description: 'New description.' },
|
|
78
|
+
homepage: { type: 'string', description: 'New homepage URL.' },
|
|
79
|
+
private: { type: 'boolean', description: 'Change visibility.' },
|
|
80
|
+
archived: { type: 'boolean', description: 'Archive or unarchive.' },
|
|
81
|
+
topics: { type: 'array', items: { type: 'string' }, description: 'Full replacement list of topics.' },
|
|
82
|
+
}),
|
|
83
|
+
async (args) => {
|
|
84
|
+
const { owner, repo } = await asRepo(args, liveConfig())
|
|
85
|
+
return editRepo(await client(), {
|
|
86
|
+
owner, repo,
|
|
87
|
+
description: args.description,
|
|
88
|
+
homepage: args.homepage,
|
|
89
|
+
private: args.private,
|
|
90
|
+
archived: args.archived,
|
|
91
|
+
topics: args.topics,
|
|
92
|
+
})
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
// -------------------------------------------------------------------- releases
|
|
97
|
+
|
|
98
|
+
tool(
|
|
99
|
+
'gh_release_list',
|
|
100
|
+
'List GitHub releases of a repository, newest first. Read-only. Use it to check whether a version was already released and which release is marked latest.',
|
|
101
|
+
repoParams({ limit: { type: 'number', description: 'How many releases to return (1-100, default 30).' } }),
|
|
102
|
+
async (args) => {
|
|
103
|
+
const { owner, repo } = await asRepo(args, liveConfig())
|
|
104
|
+
const result = await listReleases(await client(), { owner, repo, limit: args.limit })
|
|
105
|
+
return { releases: result.releases, count: result.count }
|
|
106
|
+
},
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
tool(
|
|
110
|
+
'gh_release_view',
|
|
111
|
+
'Read one GitHub release by tag: name, draft/prerelease flags, notes, assets and URLs. Read-only.',
|
|
112
|
+
repoParams({ tag: { type: 'string', description: 'Release tag, e.g. "v1.2.3".' } }),
|
|
113
|
+
async (args) => {
|
|
114
|
+
const { owner, repo } = await asRepo(args, liveConfig())
|
|
115
|
+
return getRelease(await client(), { owner, repo, tag: args.tag })
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
tool(
|
|
120
|
+
'gh_release_create',
|
|
121
|
+
'Create a GitHub release for a tag (the tag may already exist; pass target to place it on a commit). This is a public-channel write: use it only for an approved release.',
|
|
122
|
+
repoParams({
|
|
123
|
+
tag: { type: 'string', description: 'Tag to release, e.g. "v1.2.3".' },
|
|
124
|
+
name: { type: 'string', description: 'Release title; defaults to the tag.' },
|
|
125
|
+
body: { type: 'string', description: 'Release notes in markdown.' },
|
|
126
|
+
target: { type: 'string', description: 'Commit SHA or branch to place the tag on.' },
|
|
127
|
+
draft: { type: 'boolean', description: 'Create as a draft (default false).' },
|
|
128
|
+
prerelease: { type: 'boolean', description: 'Mark as a pre-release (default false).' },
|
|
129
|
+
generateNotes: { type: 'boolean', description: 'Let GitHub generate notes from commits.' },
|
|
130
|
+
}),
|
|
131
|
+
async (args) => {
|
|
132
|
+
const { owner, repo } = await asRepo(args, liveConfig())
|
|
133
|
+
return createRelease(await client(), {
|
|
134
|
+
owner, repo,
|
|
135
|
+
tag: args.tag,
|
|
136
|
+
name: args.name,
|
|
137
|
+
body: args.body,
|
|
138
|
+
draft: args.draft,
|
|
139
|
+
prerelease: args.prerelease,
|
|
140
|
+
target: args.target,
|
|
141
|
+
generateNotes: args.generateNotes,
|
|
142
|
+
})
|
|
143
|
+
},
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
tool(
|
|
147
|
+
'gh_release_edit',
|
|
148
|
+
'Edit a GitHub release: title, notes, draft/prerelease flags, and which release is marked latest (makeLatest). Use makeLatest: true after publishing a new version so the repository badge does not stay on an older release. For a DRAFT release pass releaseId (returned by gh_release_create): GitHub hides drafts from the by-tag lookup.',
|
|
149
|
+
repoParams({
|
|
150
|
+
tag: { type: 'string', description: 'Release tag to edit.' },
|
|
151
|
+
releaseId: { type: 'number', description: 'Release id from gh_release_create or gh_release_list; required for a draft.' },
|
|
152
|
+
name: { type: 'string', description: 'New title.' },
|
|
153
|
+
body: { type: 'string', description: 'New notes.' },
|
|
154
|
+
draft: { type: 'boolean', description: 'Draft flag.' },
|
|
155
|
+
prerelease: { type: 'boolean', description: 'Pre-release flag.' },
|
|
156
|
+
makeLatest: { type: 'boolean', description: 'true marks this release as latest; false unmarks it.' },
|
|
157
|
+
}),
|
|
158
|
+
async (args) => {
|
|
159
|
+
const { owner, repo } = await asRepo(args, liveConfig())
|
|
160
|
+
return editRelease(await client(), {
|
|
161
|
+
owner, repo,
|
|
162
|
+
tag: args.tag,
|
|
163
|
+
releaseId: args.releaseId,
|
|
164
|
+
name: args.name,
|
|
165
|
+
body: args.body,
|
|
166
|
+
draft: args.draft,
|
|
167
|
+
prerelease: args.prerelease,
|
|
168
|
+
makeLatest: args.makeLatest,
|
|
169
|
+
})
|
|
170
|
+
},
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
tool(
|
|
174
|
+
'gh_release_delete',
|
|
175
|
+
'Delete a GitHub release. Requires confirm: true. The git tag itself is not removed — use gh_tag_delete for that. For a DRAFT release pass releaseId: GitHub hides drafts from the by-tag lookup.',
|
|
176
|
+
repoParams({
|
|
177
|
+
tag: { type: 'string', description: 'Release tag to delete.' },
|
|
178
|
+
releaseId: { type: 'number', description: 'Release id, when the tag is unknown.' },
|
|
179
|
+
confirm: { type: 'boolean', description: 'Must be true to actually delete.' },
|
|
180
|
+
}),
|
|
181
|
+
async (args) => {
|
|
182
|
+
if (args.confirm !== true) throw new Error('refusing to delete a release without confirm: true')
|
|
183
|
+
const { owner, repo } = await asRepo(args, liveConfig())
|
|
184
|
+
return deleteRelease(await client(), { owner, repo, tag: args.tag, releaseId: args.releaseId })
|
|
185
|
+
},
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
// ------------------------------------------------------------------------ tags
|
|
189
|
+
|
|
190
|
+
tool(
|
|
191
|
+
'gh_tag_list',
|
|
192
|
+
'List git tags of a repository with their commit SHAs. Read-only. Use it to check whether the tag for a release already exists.',
|
|
193
|
+
repoParams({ limit: { type: 'number', description: 'How many tags to return (default 100).' } }),
|
|
194
|
+
async (args) => {
|
|
195
|
+
const { owner, repo } = await asRepo(args, liveConfig())
|
|
196
|
+
const result = await listTags(await client(), { owner, repo, limit: args.limit })
|
|
197
|
+
return { tags: result.tags, count: result.count }
|
|
198
|
+
},
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
tool(
|
|
202
|
+
'gh_tag_create',
|
|
203
|
+
'Create a git tag. Without message it is lightweight; with message GitHub creates an annotated tag object first. Pass sha to tag a specific commit, otherwise HEAD is tagged.',
|
|
204
|
+
repoParams({
|
|
205
|
+
tag: { type: 'string', description: 'Tag name, e.g. "v1.2.3".' },
|
|
206
|
+
sha: { type: 'string', description: 'Commit SHA to tag; defaults to HEAD.' },
|
|
207
|
+
message: { type: 'string', description: 'Annotation message; presence makes the tag annotated.' },
|
|
208
|
+
}),
|
|
209
|
+
async (args) => {
|
|
210
|
+
const { owner, repo } = await asRepo(args, liveConfig())
|
|
211
|
+
return createTag(await client(), { owner, repo, tag: args.tag, sha: args.sha, message: args.message })
|
|
212
|
+
},
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
tool(
|
|
216
|
+
'gh_tag_delete',
|
|
217
|
+
'Delete a git tag. Requires confirm: true.',
|
|
218
|
+
repoParams({
|
|
219
|
+
tag: { type: 'string', description: 'Tag to delete.' },
|
|
220
|
+
confirm: { type: 'boolean', description: 'Must be true to actually delete.' },
|
|
221
|
+
}),
|
|
222
|
+
async (args) => {
|
|
223
|
+
if (args.confirm !== true) throw new Error('refusing to delete a tag without confirm: true')
|
|
224
|
+
const { owner, repo } = await asRepo(args, liveConfig())
|
|
225
|
+
return deleteTag(await client(), { owner, repo, tag: args.tag })
|
|
226
|
+
},
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
// ------------------------------------------------------------------------- api
|
|
230
|
+
|
|
231
|
+
tool(
|
|
232
|
+
'gh_api',
|
|
233
|
+
'Call the GitHub API directly for anything the other tools do not cover (refs, rulesets, orgs, gists). Reads are free; POST/PATCH/PUT/DELETE require confirm: true, and deleting a repository, transferring one, or deleting an organization is refused outright. Pass graphql to run a GraphQL query instead of a REST path.',
|
|
234
|
+
{
|
|
235
|
+
method: { type: 'string', description: 'HTTP method: GET (default), POST, PATCH, PUT, DELETE.' },
|
|
236
|
+
path: { type: 'string', description: 'REST path relative to the API base, e.g. "/repos/owner/repo/git/refs".' },
|
|
237
|
+
query: { type: 'object', additionalProperties: true, description: 'Query string parameters.' },
|
|
238
|
+
body: { type: 'object', additionalProperties: true, description: 'JSON body for POST/PATCH/PUT, or GraphQL variables.' },
|
|
239
|
+
graphql: { type: 'string', description: 'GraphQL query; when set, path/method are ignored.' },
|
|
240
|
+
confirm: { type: 'boolean', description: 'Required for state-changing calls.' },
|
|
241
|
+
},
|
|
242
|
+
async (args) => {
|
|
243
|
+
const result = await callApi(await client(), {
|
|
244
|
+
method: args.method,
|
|
245
|
+
path: args.path,
|
|
246
|
+
query: args.query,
|
|
247
|
+
body: args.body,
|
|
248
|
+
confirm: args.confirm,
|
|
249
|
+
graphql: args.graphql,
|
|
250
|
+
})
|
|
251
|
+
return {
|
|
252
|
+
method: result.method || 'GRAPHQL',
|
|
253
|
+
mutating: Boolean(result.mutating),
|
|
254
|
+
status: result.status,
|
|
255
|
+
data: result.data,
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
)
|
|
259
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Background review tools.
|
|
2
|
+
//
|
|
3
|
+
// A review that takes a while should not hold a turn. `gh_review_job` starts one and answers
|
|
4
|
+
// immediately with a job id; `gh_review_job_status` reports the record. When the host exposes
|
|
5
|
+
// a job registry the work runs there (visible and cancellable in the UI); otherwise it runs
|
|
6
|
+
// in this plugin, which is why the tool works either way.
|
|
7
|
+
|
|
8
|
+
import { ciRun } from './pulls.js'
|
|
9
|
+
|
|
10
|
+
const REPOSITORY = {
|
|
11
|
+
repository: { type: 'string', description: 'Repository as "owner/repo". Omit to use the default from settings or the checkout.' },
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function registerJobTools({ tool, asRepo, client, liveConfig, reviewRules, jobs }) {
|
|
15
|
+
const repoParams = (extra = {}) => ({ ...REPOSITORY, ...extra })
|
|
16
|
+
|
|
17
|
+
tool(
|
|
18
|
+
'gh_review_job',
|
|
19
|
+
'Start a background review of a pull request and return its job id immediately. Works through the host job registry when one is composed, otherwise inside the plugin. Use gh_review_job_status to read the result.',
|
|
20
|
+
repoParams({
|
|
21
|
+
number: { type: 'number', description: 'Pull request number.' },
|
|
22
|
+
maxDiffChars: { type: 'number', description: 'Cap for the diff text (default 60000).' },
|
|
23
|
+
label: { type: 'string', description: 'Human label for the job.' },
|
|
24
|
+
}),
|
|
25
|
+
async (args) => {
|
|
26
|
+
const { owner, repo } = await asRepo(args, liveConfig())
|
|
27
|
+
const cfg = liveConfig()
|
|
28
|
+
const id = jobs.start({
|
|
29
|
+
repo: `${owner}/${repo}`,
|
|
30
|
+
pr: args.number,
|
|
31
|
+
label: args.label,
|
|
32
|
+
owner: args.owner,
|
|
33
|
+
timeoutMs: Number(cfg.reviewJobTimeoutMs) || 120000,
|
|
34
|
+
maxDiffChars: args.maxDiffChars,
|
|
35
|
+
rules: typeof reviewRules === 'function' ? reviewRules() : undefined,
|
|
36
|
+
client,
|
|
37
|
+
ciRun,
|
|
38
|
+
})
|
|
39
|
+
return { jobId: id, repo: `${owner}/${repo}`, number: args.number, note: 'The review runs in the background; read it with gh_review_job_status.' }
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
tool(
|
|
44
|
+
'gh_review_job_status',
|
|
45
|
+
'Read a background review job: running, done with the report, or failed with the reason. Read-only.',
|
|
46
|
+
{
|
|
47
|
+
jobId: { type: 'string', description: 'Job id returned by gh_review_job.' },
|
|
48
|
+
all: { type: 'boolean', description: 'List the recent jobs of this session instead of one.' },
|
|
49
|
+
},
|
|
50
|
+
async (args) => {
|
|
51
|
+
if (args.all) return { jobs: jobs.list() }
|
|
52
|
+
const record = jobs.get(args.jobId)
|
|
53
|
+
if (!record) return { jobId: args.jobId, status: 'unknown', note: 'No such job in this session (records are bounded and kept in memory).' }
|
|
54
|
+
return { jobId: args.jobId, status: record.status, report: record.report, error: record.error }
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Mirror tool registrations: a read-only plan and a sanitized publication.
|
|
2
|
+
//
|
|
3
|
+
// These work on a local checkout: the caller names the working copy, the source ref and
|
|
4
|
+
// the mirror remote. `gh_mirror_publish` refuses to run without confirm: true, refuses
|
|
5
|
+
// a plan with a forbidden path in the allowlist or a missing product file, and always
|
|
6
|
+
// pushes a fast-forward on top of the mirror branch — never a force.
|
|
7
|
+
|
|
8
|
+
import { planMirror, publishMirror, DEFAULT_ALLOW, DEFAULT_FORBIDDEN, DEFAULT_REQUIRED } from './mirror.js'
|
|
9
|
+
|
|
10
|
+
const MIRROR_PARAMS = {
|
|
11
|
+
cwd: { type: 'string', description: 'Absolute path of the local checkout to publish from. Defaults to the session working directory.' },
|
|
12
|
+
ref: { type: 'string', description: 'Source ref to publish, e.g. origin/main (default).' },
|
|
13
|
+
mirror: { type: 'string', description: 'Mirror remote name (default github).' },
|
|
14
|
+
branch: { type: 'string', description: 'Mirror branch (default main).' },
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function registerMirrorTools({ tool, git, defaultCwd }) {
|
|
18
|
+
const where = (args) => ({ cwd: args.cwd || defaultCwd(), ref: args.ref || 'origin/main' })
|
|
19
|
+
|
|
20
|
+
tool(
|
|
21
|
+
'gh_mirror_check',
|
|
22
|
+
'Show what a sanitized GitHub mirror publication would contain: the product files from the package manifest that would be published, how many files would stay behind, and whether the publication must be refused (a forbidden path in the allowlist, or a missing product file). Read-only: nothing is written or pushed.',
|
|
23
|
+
{
|
|
24
|
+
...MIRROR_PARAMS,
|
|
25
|
+
allow: { type: 'array', items: { type: 'string' }, description: 'Override the allowlist (defaults to the package manifest "files" plus README/LICENSE/CHANGELOG).' },
|
|
26
|
+
required: { type: 'array', items: { type: 'string' }, description: 'Files that must be present (default package.json and README.md).' },
|
|
27
|
+
},
|
|
28
|
+
async (args) => {
|
|
29
|
+
const plan = await planMirror({
|
|
30
|
+
git,
|
|
31
|
+
...where(args),
|
|
32
|
+
allow: args.allow,
|
|
33
|
+
required: args.required && args.required.length ? args.required : DEFAULT_REQUIRED,
|
|
34
|
+
forbidden: DEFAULT_FORBIDDEN,
|
|
35
|
+
})
|
|
36
|
+
return {
|
|
37
|
+
mirror: `${args.mirror || 'github'}/${args.branch || 'main'}`,
|
|
38
|
+
sourceCommit: plan.sha,
|
|
39
|
+
willPublish: plan.publish,
|
|
40
|
+
willPublishCount: plan.publish.length,
|
|
41
|
+
willDropCount: plan.drop.length,
|
|
42
|
+
refused: plan.refused,
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
tool(
|
|
48
|
+
'gh_mirror_publish',
|
|
49
|
+
'Publish the sanitized product tree to the GitHub mirror: one commit on top of the mirror branch, pushed as a fast-forward (never a force, mirror history is not rewritten). Refuses when the allowlist contains a forbidden path or a required product file is missing. Pass dryRun: true to see the plan without writing; publishing itself needs confirm: true.',
|
|
50
|
+
{
|
|
51
|
+
...MIRROR_PARAMS,
|
|
52
|
+
allow: { type: 'array', items: { type: 'string' }, description: 'Override the allowlist.' },
|
|
53
|
+
required: { type: 'array', items: { type: 'string' }, description: 'Files that must be present.' },
|
|
54
|
+
message: { type: 'string', description: 'Commit message for the mirror commit.' },
|
|
55
|
+
dryRun: { type: 'boolean', description: 'Plan only: nothing is committed or pushed.' },
|
|
56
|
+
confirm: { type: 'boolean', description: 'Must be true to write to the mirror.' },
|
|
57
|
+
},
|
|
58
|
+
async (args) => {
|
|
59
|
+
const dryRun = args.dryRun === true
|
|
60
|
+
if (!dryRun && args.confirm !== true) {
|
|
61
|
+
throw new Error('refusing to publish to a mirror without confirm: true (or pass dryRun: true to preview)')
|
|
62
|
+
}
|
|
63
|
+
const result = await publishMirror({
|
|
64
|
+
git,
|
|
65
|
+
...where(args),
|
|
66
|
+
mirror: args.mirror || 'github',
|
|
67
|
+
branch: args.branch || 'main',
|
|
68
|
+
allow: args.allow && args.allow.length ? args.allow : undefined,
|
|
69
|
+
required: args.required && args.required.length ? args.required : DEFAULT_REQUIRED,
|
|
70
|
+
forbidden: DEFAULT_FORBIDDEN,
|
|
71
|
+
message: args.message,
|
|
72
|
+
dryRun,
|
|
73
|
+
})
|
|
74
|
+
return {
|
|
75
|
+
mirror: result.mirror,
|
|
76
|
+
pushed: result.pushed,
|
|
77
|
+
commit: result.commit,
|
|
78
|
+
parent: result.parent,
|
|
79
|
+
correspondence: result.correspondence,
|
|
80
|
+
willPublishCount: result.publish.length,
|
|
81
|
+
willDropCount: result.drop.length,
|
|
82
|
+
dryRun,
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export { DEFAULT_ALLOW, DEFAULT_FORBIDDEN, DEFAULT_REQUIRED }
|