1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-01-18 05:18:24 +02:00

feat(npm): allow to publish artifact to registry (#2871)

* add new paraeters

* update generated sources

* run npm publish

* add repositoryUrl parameter

* handle registry credentials

* rename parameter

* handle base64encoding

* remove vault reference

* make username secret

* add publish method

* use publish method

* use dedicated registry

* use dry run

* fix

* prepend path

* fix workdir

* move code to npm package

* do changes

* update dependencies

* correct property init

* remomve dry-run

* regenerate

* add mock

* add logging

* add debug log

* dry-run

* remove try run

* remove append

* add debug outut

* change

* add debug output

* changes

* cleanup

* use different auth property

* add credential utils

* add debug log outputs

* remove auth handling & reuse writeFile

* rename

* fix debug output

* remove comments

* update comment

* rename function

* update docs

* update generated files

* handle npm ignore

* remove commented code

* add debug output
This commit is contained in:
Christopher Fenner 2021-07-15 14:46:04 +02:00 committed by GitHub
parent 4922a75ac1
commit f78777f784
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 445 additions and 1 deletions

View File

@ -41,5 +41,22 @@ func runNpmExecuteScripts(npmExecutor npm.Executor, config *npmExecuteScriptsOpt
}
}
return npmExecutor.RunScriptsInAllPackages(config.RunScripts, nil, config.ScriptOptions, config.VirtualFrameBuffer, config.BuildDescriptorExcludeList, config.BuildDescriptorList)
err := npmExecutor.RunScriptsInAllPackages(config.RunScripts, nil, config.ScriptOptions, config.VirtualFrameBuffer, config.BuildDescriptorExcludeList, config.BuildDescriptorList)
if err != nil {
return err
}
if config.Publish {
packageJSONFiles, err := npmExecutor.FindPackageJSONFilesWithExcludes(config.BuildDescriptorExcludeList)
if err != nil {
return err
}
err = npmExecutor.PublishAllPackages(packageJSONFiles, config.RepositoryURL, config.RepositoryUsername, config.RepositoryPassword)
if err != nil {
return err
}
}
return nil
}

View File

@ -23,6 +23,10 @@ type npmExecuteScriptsOptions struct {
BuildDescriptorExcludeList []string `json:"buildDescriptorExcludeList,omitempty"`
BuildDescriptorList []string `json:"buildDescriptorList,omitempty"`
CreateBOM bool `json:"createBOM,omitempty"`
Publish bool `json:"publish,omitempty"`
RepositoryURL string `json:"repositoryUrl,omitempty"`
RepositoryPassword string `json:"repositoryPassword,omitempty"`
RepositoryUsername string `json:"repositoryUsername,omitempty"`
}
// NpmExecuteScriptsCommand Execute npm run scripts on all npm packages in a project
@ -54,6 +58,8 @@ func NpmExecuteScriptsCommand() *cobra.Command {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}
log.RegisterSecret(stepConfig.RepositoryPassword)
log.RegisterSecret(stepConfig.RepositoryUsername)
if len(GeneralConfig.HookConfig.SentryConfig.Dsn) > 0 {
sentryHook := log.NewSentryHook(GeneralConfig.HookConfig.SentryConfig.Dsn, GeneralConfig.CorrelationID)
@ -108,6 +114,10 @@ func addNpmExecuteScriptsFlags(cmd *cobra.Command, stepConfig *npmExecuteScripts
cmd.Flags().StringSliceVar(&stepConfig.BuildDescriptorExcludeList, "buildDescriptorExcludeList", []string{`deployment/**`}, "List of build descriptors and therefore modules to exclude from execution of the npm scripts. The elements can either be a path to the build descriptor or a pattern.")
cmd.Flags().StringSliceVar(&stepConfig.BuildDescriptorList, "buildDescriptorList", []string{}, "List of build descriptors and therefore modules for execution of the npm scripts. The elements have to be paths to the build descriptors. **If set, buildDescriptorExcludeList will be ignored.**")
cmd.Flags().BoolVar(&stepConfig.CreateBOM, "createBOM", false, "Create a BOM xml using CycloneDX.")
cmd.Flags().BoolVar(&stepConfig.Publish, "publish", false, "Configures npm to publish the artifact to a repository.")
cmd.Flags().StringVar(&stepConfig.RepositoryURL, "repositoryUrl", os.Getenv("PIPER_repositoryUrl"), "Url to the repository to which the project artifacts should be published.")
cmd.Flags().StringVar(&stepConfig.RepositoryPassword, "repositoryPassword", os.Getenv("PIPER_repositoryPassword"), "Password for the repository to which the project artifacts should be published.")
cmd.Flags().StringVar(&stepConfig.RepositoryUsername, "repositoryUsername", os.Getenv("PIPER_repositoryUsername"), "Username for the repository to which the project artifacts should be published.")
}
@ -197,6 +207,57 @@ func npmExecuteScriptsMetadata() config.StepData {
Aliases: []config.Alias{},
Default: false,
},
{
Name: "publish",
ResourceRef: []config.ResourceReference{},
Scope: []string{"STEPS", "STAGES", "PARAMETERS"},
Type: "bool",
Mandatory: false,
Aliases: []config.Alias{},
Default: false,
},
{
Name: "repositoryUrl",
ResourceRef: []config.ResourceReference{
{
Name: "commonPipelineEnvironment",
Param: "custom/repositoryUrl",
},
},
Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
Default: os.Getenv("PIPER_repositoryUrl"),
},
{
Name: "repositoryPassword",
ResourceRef: []config.ResourceReference{
{
Name: "commonPipelineEnvironment",
Param: "custom/repositoryPassword",
},
},
Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
Default: os.Getenv("PIPER_repositoryPassword"),
},
{
Name: "repositoryUsername",
ResourceRef: []config.ResourceReference{
{
Name: "commonPipelineEnvironment",
Param: "custom/repositoryUsername",
},
},
Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
Default: os.Getenv("PIPER_repositoryUsername"),
},
},
},
Containers: []config.Container{

50
pkg/npm/config.go Normal file
View File

@ -0,0 +1,50 @@
package npm
import (
"io/ioutil"
"path/filepath"
"strings"
"github.com/magiconair/properties"
"github.com/pkg/errors"
)
const (
configFilename = ".npmrc"
)
var (
propertiesLoadFile = properties.LoadFile
)
func NewNPMRC(path string) NPMRC {
if !strings.HasSuffix(path, configFilename) {
path = filepath.Join(path, configFilename)
}
return NPMRC{filepath: path, values: properties.NewProperties()}
}
type NPMRC struct {
filepath string
values *properties.Properties
}
func (rc *NPMRC) Write() error {
if err := ioutil.WriteFile(rc.filepath, []byte(rc.values.String()), 0644); err != nil {
return errors.Wrapf(err, "failed to write %s", rc.filepath)
}
return nil
}
func (rc *NPMRC) Load() error {
values, err := propertiesLoadFile(rc.filepath, properties.UTF8)
if err != nil {
return err
}
rc.values = values
return nil
}
func (rc *NPMRC) Set(key, value string) {
rc.values.Set(key, value)
}

58
pkg/npm/config_test.go Normal file
View File

@ -0,0 +1,58 @@
package npm
import (
"reflect"
"testing"
"github.com/magiconair/properties"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func TestNewNPMRC(t *testing.T) {
type args struct {
path string
}
tests := []struct {
name string
args args
want string
}{
{name: "current dir", args: args{""}, want: configFilename},
{name: "sub dir", args: args{mock.Anything}, want: mock.Anything + "/.npmrc"},
{name: "file path in current dir", args: args{".npmrc"}, want: ".npmrc"},
{name: "file path in sub dir", args: args{mock.Anything + "/.npmrc"}, want: mock.Anything + "/.npmrc"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := NewNPMRC(tt.args.path); !reflect.DeepEqual(got.filepath, tt.want) {
t.Errorf("NewNPMRC().filepath = %v, want %v", got.filepath, tt.want)
}
})
}
}
func mockLoadProperties(t *testing.T, result *properties.Properties, err error) func(filename string, enc properties.Encoding) (*properties.Properties, error) {
return func(filename string, enc properties.Encoding) (*properties.Properties, error) {
return result, err
}
}
func TestLoad(t *testing.T) {
// init
config := NewNPMRC("")
new := properties.NewProperties()
new.Set("test", "anything")
propertiesLoadFile = mockLoadProperties(t, new, nil)
require.NotEmpty(t, new.Keys())
require.Empty(t, config.values.Keys())
// test
err := config.Load()
// assert
assert.NoError(t, err)
assert.NotEmpty(t, config.values.Keys())
}

56
pkg/npm/ignore.go Normal file
View File

@ -0,0 +1,56 @@
package npm
import (
"bufio"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
)
const (
ignoreFilename = ".npmignore"
)
func NewNPMIgnore(path string) NPMIgnore {
if !strings.HasSuffix(path, ignoreFilename) {
path = filepath.Join(path, ignoreFilename)
}
return NPMIgnore{filepath: path, values: []string{}}
}
type NPMIgnore struct {
filepath string
values []string
}
func (ignorefile *NPMIgnore) Write() error {
content := strings.Join(ignorefile.values, "\n")
if err := ioutil.WriteFile(ignorefile.filepath, []byte(content+"\n"), 0644); err != nil {
return errors.Wrapf(err, "failed to write %s", ignorefile.filepath)
}
return nil
}
func (ignorefile *NPMIgnore) Load() error {
file, err := os.Open(ignorefile.filepath)
if err != nil {
return err
}
defer file.Close()
var lines []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
ignorefile.values = lines
return scanner.Err()
}
func (ignorefile *NPMIgnore) Add(value string) {
ignorefile.values = append(ignorefile.values, value)
}

View File

@ -120,3 +120,8 @@ func (n *NpmExecutorMock) SetNpmRegistries() error {
func (n *NpmExecutorMock) CreateBOM(packageJSONFiles []string) error {
return nil
}
// CreateBOM mock implementation
func (n *NpmExecutorMock) PublishAllPackages(packageJSONFiles []string, registry, username, password string) error {
return nil
}

View File

@ -26,6 +26,7 @@ type Executor interface {
FindPackageJSONFilesWithScript(packageJSONFiles []string, script string) ([]string, error)
RunScriptsInAllPackages(runScripts []string, runOptions []string, scriptOptions []string, virtualFrameBuffer bool, excludeList []string, packagesList []string) error
InstallAllDependencies(packageJSONFiles []string) error
PublishAllPackages(packageJSONFiles []string, registry, username, password string) error
SetNpmRegistries() error
CreateBOM(packageJSONFiles []string) error
}

94
pkg/npm/publish.go Normal file
View File

@ -0,0 +1,94 @@
package npm
import (
"fmt"
"path/filepath"
"github.com/pkg/errors"
"github.com/SAP/jenkins-library/pkg/log"
CredentialUtils "github.com/SAP/jenkins-library/pkg/piperutils"
FileUtils "github.com/SAP/jenkins-library/pkg/piperutils"
)
// PublishAllPackages executes npm publish for all package.json files defined in packageJSONFiles list
func (exec *Execute) PublishAllPackages(packageJSONFiles []string, registry, username, password string) error {
for _, packageJSON := range packageJSONFiles {
fileExists, err := exec.Utils.FileExists(packageJSON)
if err != nil {
return fmt.Errorf("cannot check if '%s' exists: %w", packageJSON, err)
}
if !fileExists {
return fmt.Errorf("package.json file '%s' not found: %w", packageJSON, err)
}
err = exec.publish(packageJSON, registry, username, password)
if err != nil {
return err
}
}
return nil
}
// publish executes npm publish for package.json
func (exec *Execute) publish(packageJSON, registry, username, password string) error {
execRunner := exec.Utils.GetExecRunner()
npmignore := NewNPMIgnore(filepath.Dir(packageJSON))
if exists, err := FileUtils.FileExists(npmignore.filepath); exists {
if err != nil {
return errors.Wrapf(err, "failed to check for existing %s file", npmignore.filepath)
}
log.Entry().Debugf("loading existing %s file", npmignore.filepath)
if err = npmignore.Load(); err != nil {
return errors.Wrapf(err, "failed to read existing %s file", npmignore.filepath)
}
} else {
log.Entry().Debug("creating .npmignore file")
}
log.Entry().Debug("adding **/piper")
npmignore.Add("**/piper")
log.Entry().Debug("adding **/sap-piper")
npmignore.Add("**/sap-piper")
// update .npmrc
if err := npmignore.Write(); err != nil {
return errors.Wrapf(err, "failed to update %s file", npmignore.filepath)
}
if len(registry) > 0 {
npmrc := NewNPMRC(filepath.Dir(packageJSON))
// check existing .npmrc file
if exists, err := FileUtils.FileExists(npmrc.filepath); exists {
if err != nil {
return errors.Wrapf(err, "failed to check for existing %s file", npmrc.filepath)
}
log.Entry().Debugf("loading existing %s file", npmrc.filepath)
if err = npmrc.Load(); err != nil {
return errors.Wrapf(err, "failed to read existing %s file", npmrc.filepath)
}
} else {
log.Entry().Debug("creating .npmrc file")
}
// set registry
log.Entry().Debugf("adding registry %s", registry)
npmrc.Set("registry", registry)
// set registry auth
if len(username) > 0 && len(password) > 0 {
log.Entry().Debug("adding registry credentials")
npmrc.Set("_auth", CredentialUtils.EncodeUsernamePassword(username, password))
npmrc.Set("always-auth", "true")
}
// update .npmrc
if err := npmrc.Write(); err != nil {
return errors.Wrapf(err, "failed to update %s file", npmrc.filepath)
}
} else {
log.Entry().Debug("no registry provided")
}
err := execRunner.RunExecutable("npm", "publish", filepath.Dir(packageJSON))
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,14 @@
package piperutils
import (
"encoding/base64"
"fmt"
)
func EncodeString(token string) string {
return base64.StdEncoding.EncodeToString([]byte(token))
}
func EncodeUsernamePassword(username, password string) string {
return EncodeString(fmt.Sprintf("%s:%s", username, password))
}

View File

@ -0,0 +1,46 @@
package piperutils
import (
"testing"
)
func TestEncodeUsernamePassword(t *testing.T) {
type args struct {
username string
password string
}
tests := []struct {
name string
args args
want string
}{
{args: args{username: "anything", password: "something"}, want: "YW55dGhpbmc6c29tZXRoaW5n"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := EncodeUsernamePassword(tt.args.username, tt.args.password); got != tt.want {
t.Errorf("EncodeUsernamePassword() = %v, want %v", got, tt.want)
}
})
}
}
func TestEncodeToken(t *testing.T) {
type args struct {
token string
}
tests := []struct {
name string
args args
want string
}{
{args: args{token: "anything"}, want: "YW55dGhpbmc="},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := EncodeString(tt.args.token); got != tt.want {
t.Errorf("EncodeToken() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -74,6 +74,48 @@ spec:
- STAGES
- PARAMETERS
default: false
- name: publish
type: bool
description: Configures npm to publish the artifact to a repository.
scope:
- STEPS
- STAGES
- PARAMETERS
- name: repositoryUrl
type: string
description: Url to the repository to which the project artifacts should be published.
scope:
- GENERAL
- PARAMETERS
- STAGES
- STEPS
resourceRef:
- name: commonPipelineEnvironment
param: custom/repositoryUrl
- name: repositoryPassword
type: string
description: Password for the repository to which the project artifacts should be published.
scope:
- GENERAL
- PARAMETERS
- STAGES
- STEPS
secret: true
resourceRef:
- name: commonPipelineEnvironment
param: custom/repositoryPassword
- name: repositoryUsername
type: string
description: Username for the repository to which the project artifacts should be published.
scope:
- GENERAL
- PARAMETERS
- STAGES
- STEPS
secret: true
resourceRef:
- name: commonPipelineEnvironment
param: custom/repositoryUsername
containers:
- name: node
image: node:lts-stretch