mirror of
https://github.com/drone-plugins/drone-npm.git
synced 2026-06-04 18:23:52 +08:00
361 lines
8.1 KiB
Go
361 lines
8.1 KiB
Go
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.
|
||
return fmt.Sprintf("%s:_authToken=%s", registry.String(), 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()
|
||
}
|