dsh-plugin-cicd 0.0.0-stage → 0.5.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/LICENSE +21 -0
- package/README.md +389 -2
- package/RELEASING.md +63 -0
- package/client.js +3786 -0
- package/cordis.patch.yml +63 -0
- package/icon.svg +8 -0
- package/index.js +4662 -0
- package/lib/bilibili.mjs +822 -0
- package/lib/config-store.mjs +159 -0
- package/package.json +62 -3
- package/scripts/configure.mjs +213 -0
- package/scripts/push-via-api.ps1 +111 -0
- package/scripts/verify-bundle.mjs +320 -0
- package/tests/bilibili-check.mjs +398 -0
- package/tests/bump-e2e.mjs +352 -0
- package/tests/client-render.mjs +1267 -0
- package/tests/host-checks.mjs +521 -0
- package/tests/mount-check.mjs +297 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The version bump, end to end, against real git repositories.
|
|
4
|
+
*
|
|
5
|
+
* `version-bump` is the only route in this plugin that writes to a checkout, and it
|
|
6
|
+
* writes three things a mistake would be expensive in: a version number, a commit,
|
|
7
|
+
* and a push. So it is tested the way it runs — a scratch clone, a scratch bare
|
|
8
|
+
* origin, the real route over real HTTP — and every refusal is asserted to leave the
|
|
9
|
+
* manifest byte-identical, because the failure mode that matters is not "it said no"
|
|
10
|
+
* but "it said no and left half a bump behind".
|
|
11
|
+
*
|
|
12
|
+
* It is also the half of 发布 that used to be missing: the release workflow refuses
|
|
13
|
+
* to reuse a version that belongs to another commit, so without a bump the release
|
|
14
|
+
* dispatch can only fail.
|
|
15
|
+
*
|
|
16
|
+
* node tests/bump-e2e.mjs
|
|
17
|
+
* node /tmp/asset/package/tests/bump-e2e.mjs /tmp/asset/package
|
|
18
|
+
*
|
|
19
|
+
* Requires `git` on PATH; no network and no GitHub account.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { execFileSync } from 'node:child_process'
|
|
23
|
+
import { createServer } from 'node:http'
|
|
24
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
25
|
+
import { tmpdir } from 'node:os'
|
|
26
|
+
import { join, resolve } from 'node:path'
|
|
27
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
28
|
+
|
|
29
|
+
const here = fileURLToPath(new URL('.', import.meta.url))
|
|
30
|
+
const packageRoot = resolve(process.argv[2] ?? join(here, '..'))
|
|
31
|
+
const module = await import(pathToFileURL(join(packageRoot, 'index.js')).href)
|
|
32
|
+
|
|
33
|
+
const results = []
|
|
34
|
+
let failed = 0
|
|
35
|
+
function check(label, condition, detail = '') {
|
|
36
|
+
const pass = condition === true
|
|
37
|
+
if (!pass) failed += 1
|
|
38
|
+
results.push({ label, pass })
|
|
39
|
+
console.log(`${pass ? 'ok ' : 'FAIL'} ${label}${detail === '' ? '' : ` — ${detail}`}`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Run git in one of the fixtures, with a fixed identity and no user config. */
|
|
43
|
+
function git(cwd, args) {
|
|
44
|
+
return execFileSync('git', args, {
|
|
45
|
+
cwd,
|
|
46
|
+
encoding: 'utf8',
|
|
47
|
+
env: {
|
|
48
|
+
...process.env,
|
|
49
|
+
GIT_AUTHOR_NAME: 'bump-e2e',
|
|
50
|
+
GIT_AUTHOR_EMAIL: 'bump-e2e@example.invalid',
|
|
51
|
+
GIT_COMMITTER_NAME: 'bump-e2e',
|
|
52
|
+
GIT_COMMITTER_EMAIL: 'bump-e2e@example.invalid',
|
|
53
|
+
GIT_CONFIG_GLOBAL: join(scratch, 'gitconfig'),
|
|
54
|
+
GIT_CONFIG_SYSTEM: join(scratch, 'gitconfig'),
|
|
55
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
56
|
+
},
|
|
57
|
+
}).trim()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const scratch = mkdtempSync(join(tmpdir(), 'dsh-cicd-bump-'))
|
|
61
|
+
writeFileSync(join(scratch, 'gitconfig'), '', 'utf8')
|
|
62
|
+
/** `init.defaultBranch` moved over the years; pin it once so the assertions are about our code. */
|
|
63
|
+
git(scratch, ['config', '--file', join(scratch, 'gitconfig'), 'init.defaultBranch', 'main'])
|
|
64
|
+
|
|
65
|
+
const manifestFor = (version) => JSON.stringify({
|
|
66
|
+
name: 'fixture',
|
|
67
|
+
version,
|
|
68
|
+
meta: { title: '\u4e00\u952e\u91cd\u542f' },
|
|
69
|
+
}, null, 2) + '\n'
|
|
70
|
+
|
|
71
|
+
/** A bare origin plus a clone whose `main` tracks it. */
|
|
72
|
+
function makeCheckout({ version = '1.0.0' } = {}) {
|
|
73
|
+
const root = mkdtempSync(join(scratch, 'repo-'))
|
|
74
|
+
const origin = join(root, 'origin.git')
|
|
75
|
+
const work = join(root, 'work')
|
|
76
|
+
git(root, ['init', '--bare', origin])
|
|
77
|
+
git(root, ['clone', origin, work])
|
|
78
|
+
/* `init.defaultBranch` is pinned in the fixture config, but a git old enough to
|
|
79
|
+
ignore it would otherwise leave an unborn `master` and fail the push below —
|
|
80
|
+
which would read as a product failure instead of a fixture one. */
|
|
81
|
+
git(work, ['branch', '-M', 'main'])
|
|
82
|
+
/* The route runs `git commit` itself, inheriting the plugin process's
|
|
83
|
+
environment — so the identity has to exist in the REPOSITORY, not in this
|
|
84
|
+
file's child-process env. A CI runner has none, and the first Linux run of
|
|
85
|
+
this suite failed with "Author identity unknown" for exactly that reason. */
|
|
86
|
+
git(work, ['config', 'user.name', 'bump-e2e'])
|
|
87
|
+
git(work, ['config', 'user.email', 'bump-e2e@example.invalid'])
|
|
88
|
+
writeFileSync(join(work, 'package.json'), manifestFor(version), 'utf8')
|
|
89
|
+
git(work, ['add', '--', 'package.json'])
|
|
90
|
+
git(work, ['commit', '-m', 'init'])
|
|
91
|
+
git(work, ['push', '-u', 'origin', 'main'])
|
|
92
|
+
return { origin, work, manifest: join(work, 'package.json') }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* Mount the Host half exactly as a loader would, with the fixture as its only repo. */
|
|
96
|
+
function mountFor(checkout) {
|
|
97
|
+
const configFile = join(scratch, `repos-${Math.random().toString(36).slice(2)}.json`)
|
|
98
|
+
writeFileSync(configFile, JSON.stringify({
|
|
99
|
+
owner: 'octocat',
|
|
100
|
+
repos: [{ repo: 'octocat/fixture', localPath: checkout.work }],
|
|
101
|
+
}), 'utf8')
|
|
102
|
+
const routes = new Map()
|
|
103
|
+
const ctx = {
|
|
104
|
+
effect: (fn) => fn,
|
|
105
|
+
logger: { info: () => {} },
|
|
106
|
+
webServer: {
|
|
107
|
+
register: ({ path, handler }) => {
|
|
108
|
+
routes.set(path, handler)
|
|
109
|
+
return () => routes.delete(path)
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
module.apply(ctx, { owner: 'octocat', configFile })
|
|
114
|
+
return routes
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Serve one mounted route over loopback and return the parsed answer. */
|
|
118
|
+
async function callRoutes(routes, path, body) {
|
|
119
|
+
const server = createServer((req, res) => {
|
|
120
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1')
|
|
121
|
+
const handler = routes.get(url.pathname)
|
|
122
|
+
if (handler === undefined) {
|
|
123
|
+
res.statusCode = 404
|
|
124
|
+
res.end('{"ok":false}')
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
void handler(req, res)
|
|
128
|
+
})
|
|
129
|
+
await new Promise((settle) => server.listen(0, '127.0.0.1', settle))
|
|
130
|
+
const { port } = server.address()
|
|
131
|
+
try {
|
|
132
|
+
const response = await fetch(`http://127.0.0.1:${String(port)}${path}`, {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
headers: { 'content-type': 'application/json', origin: `http://127.0.0.1:${String(port)}` },
|
|
135
|
+
body: JSON.stringify(body),
|
|
136
|
+
})
|
|
137
|
+
return { status: response.status, payload: await response.json() }
|
|
138
|
+
} finally {
|
|
139
|
+
await new Promise((settle) => server.close(settle))
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* -- 1. The happy path ------------------------------------------------------ */
|
|
144
|
+
{
|
|
145
|
+
const checkout = makeCheckout()
|
|
146
|
+
const before = readFileSync(checkout.manifest, 'utf8')
|
|
147
|
+
const routes = mountFor(checkout)
|
|
148
|
+
const answer = await callRoutes(routes, '/api/dsh-cicd/version-bump', { repo: 'octocat/fixture', release: 'patch' })
|
|
149
|
+
|
|
150
|
+
check('a clean checkout is bumped', answer.status === 200 && answer.payload?.ok === true, JSON.stringify(answer.payload?.value ?? answer.payload))
|
|
151
|
+
check('the bump reports both versions', answer.payload?.value?.from === '1.0.0' && answer.payload?.value?.to === '1.0.1', `${String(answer.payload?.value?.from)} → ${String(answer.payload?.value?.to)}`)
|
|
152
|
+
check('the tag follows the new version', answer.payload?.value?.tag === 'v1.0.1', String(answer.payload?.value?.tag))
|
|
153
|
+
check('the branch is named', answer.payload?.value?.branch === 'main', String(answer.payload?.value?.branch))
|
|
154
|
+
check('the push is reported as done', answer.payload?.value?.pushed === true)
|
|
155
|
+
|
|
156
|
+
const rewritten = readFileSync(checkout.manifest, 'utf8')
|
|
157
|
+
check('the manifest on disk carries the new version', /"version":\s*"1\.0\.1"/.test(rewritten), rewritten.split('\n')[1] ?? '')
|
|
158
|
+
check('every other byte of the manifest is untouched', rewritten === before.replace('"version": "1.0.0"', '"version": "1.0.1"'))
|
|
159
|
+
check('the manifest still parses', JSON.parse(rewritten).version === '1.0.1')
|
|
160
|
+
check('the escaped metadata survived', rewritten.includes('\\u4e00') === before.includes('\\u4e00'))
|
|
161
|
+
|
|
162
|
+
const subject = git(checkout.work, ['log', '-1', '--pretty=%s'])
|
|
163
|
+
check('the commit is a release chore', subject === 'chore(release): v1.0.1', subject)
|
|
164
|
+
const files = git(checkout.work, ['show', '--name-only', '--pretty=format:', 'HEAD'])
|
|
165
|
+
check('the commit contains only the manifest', files === 'package.json', files)
|
|
166
|
+
check('the working tree is clean afterwards', git(checkout.work, ['status', '--porcelain']) === '')
|
|
167
|
+
const originHead = git(checkout.origin, ['log', '-1', '--pretty=%s', 'main'])
|
|
168
|
+
check('the commit reached the origin', originHead === 'chore(release): v1.0.1', originHead)
|
|
169
|
+
check('the push carried nothing else', Number(git(checkout.work, ['rev-list', '--count', 'origin/main..HEAD'])) === 0)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* -- 2. A dirty tree is refused and NOTHING is written ---------------------- */
|
|
173
|
+
{
|
|
174
|
+
const checkout = makeCheckout()
|
|
175
|
+
writeFileSync(join(checkout.work, 'scratch.txt'), 'work in progress\n', 'utf8')
|
|
176
|
+
const before = readFileSync(checkout.manifest, 'utf8')
|
|
177
|
+
const routes = mountFor(checkout)
|
|
178
|
+
const answer = await callRoutes(routes, '/api/dsh-cicd/version-bump', { repo: 'octocat/fixture', release: 'patch' })
|
|
179
|
+
|
|
180
|
+
check('a dirty tree is refused', answer.status === 409 && answer.payload?.code === 'dirty-tree', `${String(answer.status)} ${String(answer.payload?.code)}`)
|
|
181
|
+
check('the refusal counts the changes', answer.payload?.value?.dirty === 1, String(answer.payload?.value?.dirty))
|
|
182
|
+
check('the dirty refusal left the manifest alone', readFileSync(checkout.manifest, 'utf8') === before)
|
|
183
|
+
check('the dirty refusal created no commit', git(checkout.work, ['log', '-1', '--pretty=%s']) === 'init')
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* -- 3. A version that cannot be bumped is refused ------------------------- */
|
|
187
|
+
{
|
|
188
|
+
const checkout = makeCheckout({ version: '1.0.0-rc.1' })
|
|
189
|
+
const before = readFileSync(checkout.manifest, 'utf8')
|
|
190
|
+
const routes = mountFor(checkout)
|
|
191
|
+
const answer = await callRoutes(routes, '/api/dsh-cicd/version-bump', { repo: 'octocat/fixture', release: 'patch' })
|
|
192
|
+
|
|
193
|
+
check('a prerelease version is refused', answer.status === 400 && answer.payload?.code === 'unusable-version', `${String(answer.status)} ${String(answer.payload?.code)}`)
|
|
194
|
+
check('the prerelease refusal left the manifest alone', readFileSync(checkout.manifest, 'utf8') === before)
|
|
195
|
+
check('the prerelease refusal created no commit', git(checkout.work, ['log', '-1', '--pretty=%s']) === 'init')
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* -- 4. A branch behind its upstream is refused --------------------------- */
|
|
199
|
+
{
|
|
200
|
+
const checkout = makeCheckout()
|
|
201
|
+
const other = join(scratch, `other-${Math.random().toString(36).slice(2)}`)
|
|
202
|
+
git(scratch, ['clone', checkout.origin, other])
|
|
203
|
+
git(other, ['branch', '-M', 'main'])
|
|
204
|
+
writeFileSync(join(other, 'second.txt'), 'from elsewhere\n', 'utf8')
|
|
205
|
+
git(other, ['add', '--', 'second.txt'])
|
|
206
|
+
git(other, ['commit', '-m', 'elsewhere'])
|
|
207
|
+
git(other, ['push', 'origin', 'main'])
|
|
208
|
+
git(checkout.work, ['fetch', 'origin'])
|
|
209
|
+
const before = readFileSync(checkout.manifest, 'utf8')
|
|
210
|
+
const routes = mountFor(checkout)
|
|
211
|
+
const answer = await callRoutes(routes, '/api/dsh-cicd/version-bump', { repo: 'octocat/fixture', release: 'patch' })
|
|
212
|
+
|
|
213
|
+
check('a branch behind its upstream is refused', answer.status === 409 && answer.payload?.code === 'behind', `${String(answer.status)} ${String(answer.payload?.code)}`)
|
|
214
|
+
check('the behind refusal left the manifest alone', readFileSync(checkout.manifest, 'utf8') === before)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* -- 5. Unpushed commits are allowed, and reported ------------------------- */
|
|
218
|
+
{
|
|
219
|
+
const checkout = makeCheckout()
|
|
220
|
+
writeFileSync(join(checkout.work, 'extra.txt'), 'local work\n', 'utf8')
|
|
221
|
+
git(checkout.work, ['add', '--', 'extra.txt'])
|
|
222
|
+
git(checkout.work, ['commit', '-m', 'local work'])
|
|
223
|
+
const routes = mountFor(checkout)
|
|
224
|
+
const answer = await callRoutes(routes, '/api/dsh-cicd/version-bump', { repo: 'octocat/fixture', release: 'minor' })
|
|
225
|
+
|
|
226
|
+
check('an ahead branch is still bumpable', answer.status === 200 && answer.payload?.ok === true, JSON.stringify(answer.payload?.value ?? answer.payload))
|
|
227
|
+
check('the bump says how much it carried', answer.payload?.value?.carried === 1, String(answer.payload?.value?.carried))
|
|
228
|
+
check('a minor bump resets the patch', answer.payload?.value?.to === '1.1.0', String(answer.payload?.value?.to))
|
|
229
|
+
check('everything local reached the origin', Number(git(checkout.work, ['rev-list', '--count', 'origin/main..HEAD'])) === 0)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* -- 6. No local checkout is a message, not a crash ------------------------ */
|
|
233
|
+
{
|
|
234
|
+
const configFile = join(scratch, 'repos-nolocal.json')
|
|
235
|
+
writeFileSync(configFile, JSON.stringify({ owner: 'octocat', repos: [{ repo: 'octocat/fixture' }] }), 'utf8')
|
|
236
|
+
const routes = new Map()
|
|
237
|
+
module.apply({
|
|
238
|
+
effect: (fn) => fn,
|
|
239
|
+
logger: { info: () => {} },
|
|
240
|
+
webServer: { register: ({ path, handler }) => { routes.set(path, handler); return () => {} } },
|
|
241
|
+
}, { owner: 'octocat', configFile })
|
|
242
|
+
const answer = await callRoutes(routes, '/api/dsh-cicd/version-bump', { repo: 'octocat/fixture' })
|
|
243
|
+
|
|
244
|
+
check('a repository with no checkout is refused', answer.status === 400 && answer.payload?.code === 'no-checkout', `${String(answer.status)} ${String(answer.payload?.code)}`)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* -- 7. The release preflight refuses a version that is taken -------------- */
|
|
248
|
+
{
|
|
249
|
+
const checkout = makeCheckout()
|
|
250
|
+
const routes = mountFor(checkout)
|
|
251
|
+
/* The fixture's upstream is unreachable for `gh`, so the preflight cannot read
|
|
252
|
+
GitHub — which is exactly the case that must NOT block: an unreadable answer
|
|
253
|
+
dispatches as before rather than refusing a release that would have worked. */
|
|
254
|
+
const answer = await callRoutes(routes, '/api/dsh-cicd/dispatch', { repo: 'octocat/fixture', workflow: 'release.yml', inputs: { draft: 'true' } })
|
|
255
|
+
check('an unreadable preflight does not block the dispatch', answer.payload?.code !== 'version-taken', `${String(answer.status)} ${String(answer.payload?.code ?? answer.payload?.message ?? '')}`)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/* -- 8. 提交: the step 构建 and 发布 cannot do without ------------------------ */
|
|
259
|
+
/*
|
|
260
|
+
* A release builds the PUSHED commit, so work that is only in the working tree — or
|
|
261
|
+
* only on this machine — is silently absent from the released package. That is what
|
|
262
|
+
* this route exists to fix, and it is the only action in the plugin that commits
|
|
263
|
+
* everything, so both halves are checked against a real origin.
|
|
264
|
+
*/
|
|
265
|
+
{
|
|
266
|
+
const checkout = makeCheckout()
|
|
267
|
+
writeFileSync(join(checkout.work, 'notes.md'), 'a file nobody tracked before\n', 'utf8')
|
|
268
|
+
writeFileSync(checkout.manifest, manifestFor('1.0.1'), 'utf8')
|
|
269
|
+
const routes = mountFor(checkout)
|
|
270
|
+
const answer = await callRoutes(routes, '/api/dsh-cicd/commit', { repo: 'octocat/fixture', message: 'docs: a note' })
|
|
271
|
+
|
|
272
|
+
check('a dirty checkout commits and pushes', answer.status === 200 && answer.payload?.ok === true, JSON.stringify(answer.payload?.value ?? answer.payload))
|
|
273
|
+
check('the commit reached the origin', git(checkout.work, ['log', '-1', '--pretty=%s', 'origin/main']) === 'docs: a note', git(checkout.work, ['log', '-1', '--pretty=%s', 'origin/main']))
|
|
274
|
+
check('the working tree is clean afterwards', git(checkout.work, ['status', '--porcelain']) === '')
|
|
275
|
+
/* `add -A`, not `add -u`: a release needs the new files too, and a commit that
|
|
276
|
+
quietly left one behind is the same trap one level down. */
|
|
277
|
+
check('an untracked file is included, not left behind', git(checkout.work, ['ls-tree', '-r', '--name-only', 'HEAD']).includes('notes.md'))
|
|
278
|
+
check('the edit to a tracked file is included', git(checkout.work, ['show', 'HEAD:package.json']).includes('1.0.1'))
|
|
279
|
+
check('the answer lists what was committed', Array.isArray(answer.payload?.value?.files) && answer.payload.value.files.length === 2, JSON.stringify(answer.payload?.value?.files))
|
|
280
|
+
check('the answer names the branch it pushed', answer.payload?.value?.branch === 'main', String(answer.payload?.value?.branch))
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/* The message is the one thing this route cannot invent: an empty one is refused
|
|
284
|
+
before anything is written, and the changes stay where they were. */
|
|
285
|
+
{
|
|
286
|
+
const checkout = makeCheckout()
|
|
287
|
+
writeFileSync(join(checkout.work, 'notes.md'), 'x\n', 'utf8')
|
|
288
|
+
const routes = mountFor(checkout)
|
|
289
|
+
const answer = await callRoutes(routes, '/api/dsh-cicd/commit', { repo: 'octocat/fixture', message: ' ' })
|
|
290
|
+
|
|
291
|
+
check('a commit with no message is refused', answer.status === 400 && answer.payload?.code === 'message-required', `${String(answer.status)} ${String(answer.payload?.code)}`)
|
|
292
|
+
check('the refusal created no commit', git(checkout.work, ['log', '-1', '--pretty=%s']) === 'init')
|
|
293
|
+
check('the refusal left the change in place', git(checkout.work, ['status', '--porcelain']) !== '')
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/* Clean and in sync: there is nothing to do, and the route says that rather than
|
|
297
|
+
creating an empty commit or pushing nothing. */
|
|
298
|
+
{
|
|
299
|
+
const checkout = makeCheckout()
|
|
300
|
+
const routes = mountFor(checkout)
|
|
301
|
+
const answer = await callRoutes(routes, '/api/dsh-cicd/commit', { repo: 'octocat/fixture', message: 'nothing to say' })
|
|
302
|
+
|
|
303
|
+
check('a clean, in-sync checkout has nothing to commit', answer.status === 409 && answer.payload?.code === 'nothing-to-commit', `${String(answer.status)} ${String(answer.payload?.code)}`)
|
|
304
|
+
check('nothing was committed for it', git(checkout.work, ['log', '-1', '--pretty=%s']) === 'init')
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/* The other half of the same trap: the work IS committed, and it has never left this
|
|
308
|
+
machine — which a release cannot see either. An empty message means exactly that. */
|
|
309
|
+
{
|
|
310
|
+
const checkout = makeCheckout()
|
|
311
|
+
writeFileSync(join(checkout.work, 'notes.md'), 'x\n', 'utf8')
|
|
312
|
+
git(checkout.work, ['add', '-A'])
|
|
313
|
+
git(checkout.work, ['commit', '-m', 'local only'])
|
|
314
|
+
const routes = mountFor(checkout)
|
|
315
|
+
const answer = await callRoutes(routes, '/api/dsh-cicd/commit', { repo: 'octocat/fixture', message: '' })
|
|
316
|
+
|
|
317
|
+
check('a clean but unpushed branch is pushed', answer.status === 200 && answer.payload?.ok === true, JSON.stringify(answer.payload?.value ?? answer.payload))
|
|
318
|
+
check('it reports that it committed nothing new', answer.payload?.value?.committed === false)
|
|
319
|
+
check('it says how many local commits it carried', answer.payload?.value?.carried === 1, String(answer.payload?.value?.carried))
|
|
320
|
+
check('the local commit reached the origin', git(checkout.work, ['log', '-1', '--pretty=%s', 'origin/main']) === 'local only')
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/* No upstream: the push could only fail, so the refusal names the reason instead. */
|
|
324
|
+
{
|
|
325
|
+
const checkout = makeCheckout()
|
|
326
|
+
git(checkout.work, ['checkout', '-b', 'solo'])
|
|
327
|
+
writeFileSync(join(checkout.work, 'notes.md'), 'x\n', 'utf8')
|
|
328
|
+
const routes = mountFor(checkout)
|
|
329
|
+
const answer = await callRoutes(routes, '/api/dsh-cicd/commit', { repo: 'octocat/fixture', message: 'solo work' })
|
|
330
|
+
|
|
331
|
+
check('a branch with no upstream is refused', answer.status === 409 && answer.payload?.code === 'no-upstream', `${String(answer.status)} ${String(answer.payload?.code)}`)
|
|
332
|
+
check('the refusal created no commit', git(checkout.work, ['log', '-1', '--pretty=%s']) === 'init')
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/* No checkout at all, the same shape as the bump refusal above. */
|
|
336
|
+
{
|
|
337
|
+
const configFile = join(scratch, 'repos-nolocal-commit.json')
|
|
338
|
+
writeFileSync(configFile, JSON.stringify({ owner: 'octocat', repos: [{ repo: 'octocat/fixture' }] }), 'utf8')
|
|
339
|
+
const routes = new Map()
|
|
340
|
+
module.apply({
|
|
341
|
+
effect: (fn) => fn,
|
|
342
|
+
logger: { info: () => {} },
|
|
343
|
+
webServer: { register: ({ path, handler }) => { routes.set(path, handler); return () => {} } },
|
|
344
|
+
}, { owner: 'octocat', configFile })
|
|
345
|
+
const answer = await callRoutes(routes, '/api/dsh-cicd/commit', { repo: 'octocat/fixture', message: 'x' })
|
|
346
|
+
|
|
347
|
+
check('a repository with no checkout is refused here too', answer.status === 400 && answer.payload?.code === 'no-checkout', `${String(answer.status)} ${String(answer.payload?.code)}`)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
await new Promise((settle) => { rmSync(scratch, { recursive: true, force: true }); settle() })
|
|
351
|
+
console.log(`\n${results.length - failed}/${results.length} checks passed`)
|
|
352
|
+
if (failed > 0) process.exit(1)
|