-
-
Notifications
You must be signed in to change notification settings - Fork 13
/
Copy pathpublish.js
236 lines (206 loc) · 7.7 KB
/
publish.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
'use strict'
const assert = require('node:assert').strict
const pino = require('pino')
const { validate } = require('../validation')
const draft = require('./draft')
const Npm = require('../npm')
const GitDirectory = require('../git-directory')
const GitHub = require('../github')
const { editMessage } = require('../editor')
const getRepositoryUrl = require('../get-repository-url')
const { Input } = require('enquirer')
const debug = require('debug')
// TODO this could be optimized with a common schema using $ref..
const ARGS_SCHEMA = {
type: 'object',
required: ['path', 'verbose', 'major', 'remote', 'branch', 'semver', 'ghToken'],
properties: {
major: { type: 'boolean' },
help: { type: 'boolean' },
dryRun: { type: 'boolean' },
ghReleaseEdit: { type: 'boolean' },
ghReleaseDraft: { type: 'boolean' },
ghReleasePrerelease: { type: 'boolean' },
ghReleaseBody: { type: 'boolean' },
path: { type: 'string' },
tag: { type: 'string' },
remote: { type: 'string' },
branch: { type: 'string' },
ghToken: {
type: 'string',
minLength: 40
},
verbose: {
type: 'string',
enum: ['trace', 'debug', 'info', 'warn', 'error']
},
npmAccess: {
type: 'string',
enum: ['public', 'restricted']
},
npmDistTag: {
type: 'string',
pattern: '^[^v0-9][\\w]{1,12}'
},
semver: {
type: 'string',
enum: ['major', 'premajor', 'minor', 'preminor', 'patch', 'prepatch', 'prerelease']
}
}
}
const CLEAN_REPO = {
not_added: [],
conflicted: [],
created: [],
deleted: [],
modified: [],
renamed: [],
files: [],
staged: [],
ahead: 0,
behind: 0,
tracking: null
}
// TODO refactor to make optionals step (publish / push / release)
module.exports = async function (args) {
validate(args, ARGS_SCHEMA)
const logger = pino({ level: args.verbose, base: null, transport: { target: 'pino-pretty' } })
if (args.verbose === 'trace') {
debug.enable('simple-git,simple-git:*')
}
const git = GitDirectory(args.path)
const status = await git.status()
const compare = { ...status }
delete compare.current // the current branch could be named in a different way
CLEAN_REPO.tracking = `${args.remote}/${args.branch}` // your local branch must track where you will push
logger.debug('Complete repo status %s', inlineNotEmpty(status))
assert.deepStrictEqual(buildGitStatus(compare), CLEAN_REPO, 'The git repo must be clean (committed and pushed) before releasing!')
logger.debug('Building draft release..')
const releasing = await draft(args)
logger.info('Ready to release %s: %s --> %s', releasing.release, releasing.oldVersion, releasing.version)
if (releasing.lines === 0) {
throw new Error('There are ZERO commit to release!')
}
// TODO check the validity of the GH-TOKEN.. need another param with the github account:
// https://octokit.github.io/rest.js/#api-OauthAuthorizations-checkAuthorization
if (releasing.release === 'major' && args.major !== true) {
throw new Error('You can not release a major version without --major flag')
}
const npm = Npm(args.path)
await npm.ping() // check internet connection
const registry = await npm.config('registry')
const user = await npm.whoami()
logger.info('User: %s is preparing to release in registry %s', user, registry)
// check if the version we are releasing exists already
const moduleLink = `${releasing.name}@${releasing.version}`
let isAlreadyPublished = false
try {
isAlreadyPublished = (await npm.show(moduleLink, 'version')) === releasing.version
} catch {
// if the module has been never released, the `show version` throws a 404
logger.warn('Unable to find module link %s', moduleLink)
}
if (isAlreadyPublished) {
throw new Error(`The module ${moduleLink} is already published in the registry ${registry}`)
}
logger.debug('The module %s is going to be published', moduleLink)
// ? should use --allow-same-version (maybe someone want/has bumped manually)
await npm.version(releasing.version)
logger.debug('Bumped new version %s', releasing.version)
// ! ******
// ! DANGER ZONE: from this point if some error occurs we must explain to user what to do to fix
// ? publish at the end??
try {
await npm.publish({ tag: args.npmDistTag, access: args.npmAccess, otp: args.npmOtp })
} catch (err) {
if ((err.message.includes('npm ERR! code EOTP') || err.message.includes('one-time password')) && !args.silent) {
if (args.npmOtp) {
logger.error(`OTP code "${args.npmOtp}" is not valid`)
}
const prompt = new Input({ message: `OTP code is required to publish ${moduleLink} to NPM:` })
const inputtedOtp = await prompt.run()
await npm.publish({ tag: args.npmDistTag, access: args.npmAccess, otp: inputtedOtp })
} else {
throw err
}
}
logger.info('Published new module version %s', moduleLink)
let commited
try {
// NOTE: we are not creating and pushing any tags, it will be created by GitHub
// NOTE 2: If we don't bump any version we aren't committing any change (ex: first release and version already set)
await git.add('package.json')
commited = await git.commit({ message: `Bumped v${releasing.version}`, noVerify: args.noVerify })
logger.debug('Commit id %s on branch %s', commited.commit, commited.branch)
await git.push(args.remote)
logger.info('Pushed to git %s', args.remote)
} catch (error) {
logger.error(error)
const messageError = `Something went wrong pushing the package.json to git.
The 'npm publish' has been done! Check your 'git status' and if necessary run 'npm unpublish ${moduleLink}'.
Consider creating a release on GitHub by yourself with this message:\n${releasing.message}`
throw new Error(messageError)
}
try {
const repositoryUrl = getRepositoryUrl(git)
const github = GitHub(
{
auth: args.ghToken,
url: repositoryUrl
},
logger
)
if (!args.ghReleaseBody && args.ghReleaseEdit) {
logger.debug('Waiting for user to edit the release message..')
releasing.message = await editMessage(releasing.message, 'edit-release-message.md')
}
logger.debug('Creating release on GitHub..')
const githubRelease = await github.createRelease({
tag_name: `v${releasing.version}`,
target_commitish: commited.branch,
name: `v${releasing.version}`,
body: releasing.message,
draft: args.ghReleaseDraft,
prerelease: args.ghReleasePrerelease,
generate_release_notes: args.ghReleaseBody
})
logger.info('Created GitHub release: %s', (githubRelease?.data?.html_url))
} catch (error) {
logger.error(error)
const messageError = `Something went wrong creating the release on GitHub.
The 'npm publish' and 'git push' has been done!
Consider creating a release on GitHub by yourself with this message:\n${releasing.message}`
throw new Error(messageError)
}
try {
await git.pull(args.remote)
logger.debug('Pulled new tags from remote: %s', args.remote)
} catch (error) {
logger.warn('Unable to pull the new release tag due: %s', error.message)
}
return releasing
}
function inlineNotEmpty (json) {
let msg = ''
const keys = Object.keys(json)
for (const key of keys) {
const value = json[key]
// only array with atleast 1 element,
// strings with a min-length of 0
// and numbers > 0
if (
(Array.isArray(value) && value.length !== 0) ||
(typeof value === 'string' && value.length !== 0) ||
(typeof value === 'number' && value > 0)
) {
msg += `${key}=${JSON.stringify(value)} `
}
}
return msg
}
function buildGitStatus (current) {
return Object.keys(CLEAN_REPO).reduce((acc, key) => {
acc[key] = current[key]
return acc
}, {})
}