forked from drone-plugins/drone-npm
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathplugin.go
365 lines (287 loc) · 8.27 KB
/
plugin.go
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
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
package main
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/url"
"os"
"os/exec"
"os/user"
"path"
"strings"
log "github.com/sirupsen/logrus"
)
type (
// Config for the plugin.
Config struct {
Username string
Password string
Token string
Email string
Registry string
Folder string
SkipVerify bool
FailOnVersionConflict bool
Tag string
Access string
}
npmPackage struct {
Name string `json:"name"`
Version string `json:"version"`
Config npmConfig `json:"publishConfig"`
}
npmConfig struct {
Registry string `json:"registry"`
}
// Plugin values
Plugin struct {
Config Config
}
)
// GlobalRegistry defines the default NPM registry.
const GlobalRegistry = "https://registry.npmjs.org/"
// Exec executes the plugin.
func (p Plugin) Exec() error {
// write npmrc for authentication
err := writeNpmrc(p.Config)
if err != nil {
return err
}
// attempt to authenticate
err = authenticate(p.Config)
if err != nil {
return err
}
// read the package
npmPackage, err := readPackageFile(p.Config)
if err != nil {
return err
}
// determine whether to publish
publish, err := shouldPublishPackage(p.Config, npmPackage)
if err != nil {
return err
}
if publish {
log.Info("Publishing package")
// run the publish command
return runCommand(publishCommand(p.Config), p.Config.Folder)
} else {
log.Info("Not publishing package")
}
return nil
}
/// writeNpmrc creates a .npmrc in the folder for authentication
func writeNpmrc(config Config) error {
var npmrcContents string
// check for an auth token
if len(config.Token) == 0 {
// check for a username
if len(config.Username) == 0 {
return errors.New("No username provided")
}
// check for an email
if len(config.Email) == 0 {
return errors.New("No email address provided")
}
// check for a password
if len(config.Password) == 0 {
log.Warning("No password provided")
}
log.WithFields(log.Fields{
"username": config.Username,
"email": config.Email,
}).Info("Specified credentials")
npmrcContents = npmrcContentsUsernamePassword(config)
} else {
log.Info("Token credentials being used")
npmrcContents = npmrcContentsToken(config)
}
// write npmrc file
home := "/root"
user, err := user.Current()
if err == nil {
home = user.HomeDir
}
npmrcPath := path.Join(home, ".npmrc")
log.WithFields(log.Fields{
"path": npmrcPath,
}).Info("Writing npmrc")
return ioutil.WriteFile(npmrcPath, []byte(npmrcContents), 0644)
}
/// authenticate atempts to authenticate with the NPM registry.
func authenticate(config Config) error {
var cmds []*exec.Cmd
// write the version command
cmds = append(cmds, versionCommand())
// write registry command
if config.Registry != GlobalRegistry {
cmds = append(cmds, registryCommand(config.Registry))
}
// write auth command
cmds = append(cmds, alwaysAuthCommand())
// write skip verify command
if config.SkipVerify {
cmds = append(cmds, skipVerifyCommand())
}
// write whoami command to verify credentials
cmds = append(cmds, whoamiCommand())
// run commands
err := runCommands(cmds, config.Folder)
if err != nil {
return errors.New("Could not authenticate")
}
return nil
}
/// readPackageFile reads the package file at the given path.
func readPackageFile(config Config) (*npmPackage, error) {
// read the file
packagePath := path.Join(config.Folder, "package.json")
file, err := ioutil.ReadFile(packagePath)
if err != nil {
return nil, err
}
// unmarshal the json data
npm := npmPackage{}
err = json.Unmarshal(file, &npm)
if len(npm.Config.Registry) == 0 {
log.Info("No registry specified in the package.json")
npm.Config.Registry = GlobalRegistry
}
if err != nil {
return nil, err
}
// make sure values are present
if len(npm.Name) == 0 {
return nil, errors.New("No package name present")
}
if len(npm.Version) == 0 {
return nil, errors.New("No package version present")
}
// check package registry
if strings.Compare(config.Registry, npm.Config.Registry) != 0 {
return nil, fmt.Errorf("Registry values do not match .drone.yml: %s package.json: %s", config.Registry, npm.Config.Registry)
}
log.WithFields(log.Fields{
"name": npm.Name,
"version": npm.Version,
}).Info("Found package.json")
return &npm, nil
}
/// shouldPublishPackage determines if the package should be published
func shouldPublishPackage(config Config, npm *npmPackage) (bool, error) {
cmd := packageVersionsCommand(npm.Name)
cmd.Dir = config.Folder
trace(cmd)
out, err := cmd.CombinedOutput()
// see if there was an error
// if there is an error its likely due to the package never being published
if err == nil {
// parse the json output
var versions []string
err = json.Unmarshal(out, &versions)
if err != nil {
log.Debug("Could not parse into array of string. Likely single value")
var version string
err := json.Unmarshal(out, &version)
if err != nil {
return false, err
}
versions = append(versions, version)
}
for _, value := range versions {
log.WithFields(log.Fields{
"version": value,
}).Debug("Found version of package")
if strings.Compare(npm.Version, value) == 0 {
log.Info("Version found in the registry")
if config.FailOnVersionConflict {
return false, errors.New("Cannot publish package due to version conflict")
}
return false, nil
}
}
log.Info("Version not found in the registry")
} else {
log.Info("Name was not found in the registry")
}
return true, nil
}
// npmrcContentsUsernamePassword creates the contents from a username and
// password
func npmrcContentsUsernamePassword(config Config) string {
// get the base64 encoded string
authString := fmt.Sprintf("%s:%s", config.Username, config.Password)
encoded := base64.StdEncoding.EncodeToString([]byte(authString))
// create the file contents
return fmt.Sprintf("_auth = %s\nemail = %s", encoded, config.Email)
}
/// Writes npmrc contents when using a token
func npmrcContentsToken(config Config) string {
registry, _ := url.Parse(config.Registry)
registry.Scheme = "" // Reset the scheme to empty. This makes it so we will get a protocol relative URL.
registryString := registry.String()
if !strings.HasSuffix(registryString, "/") {
registryString = registryString + "/"
}
return fmt.Sprintf("%s:_authToken=%s", registryString, config.Token)
}
// versionCommand gets the npm version
func versionCommand() *exec.Cmd {
return exec.Command("npm", "--version")
}
// registryCommand sets the NPM registry.
func registryCommand(registry string) *exec.Cmd {
return exec.Command("npm", "config", "set", "registry", registry)
}
// alwaysAuthCommand forces authentication.
func alwaysAuthCommand() *exec.Cmd {
return exec.Command("npm", "config", "set", "always-auth", "true")
}
// skipVerifyCommand disables ssl verification.
func skipVerifyCommand() *exec.Cmd {
return exec.Command("npm", "config", "set", "strict-ssl", "false")
}
// whoamiCommand creates a command that gets the currently logged in user.
func whoamiCommand() *exec.Cmd {
return exec.Command("npm", "whoami")
}
// packageVersionsCommand gets the versions of the npm package.
func packageVersionsCommand(name string) *exec.Cmd {
return exec.Command("npm", "view", name, "versions", "--json")
}
// publishCommand runs the publish command
func publishCommand(config Config) *exec.Cmd {
commandArgs := []string{"publish"}
if len(config.Tag) != 0 {
commandArgs = append(commandArgs, "--tag", config.Tag)
}
if len(config.Access) != 0 {
commandArgs = append(commandArgs, "--access", config.Access)
}
return exec.Command("npm", commandArgs...)
}
// trace writes each command to standard error (preceded by a ‘$ ’) before it
// is executed. Used for debugging your build.
func trace(cmd *exec.Cmd) {
fmt.Fprintf(os.Stdout, "+ %s\n", strings.Join(cmd.Args, " "))
}
// runCommands executes the list of cmds in the given directory.
func runCommands(cmds []*exec.Cmd, dir string) error {
for _, cmd := range cmds {
err := runCommand(cmd, dir)
if err != nil {
return err
}
}
return nil
}
func runCommand(cmd *exec.Cmd, dir string) error {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Dir = dir
trace(cmd)
return cmd.Run()
}