diff --git a/.circleci/_config.jsonnet b/.circleci/_config.jsonnet index 8f8b660..4e36dac 100644 --- a/.circleci/_config.jsonnet +++ b/.circleci/_config.jsonnet @@ -10,7 +10,7 @@ local tag_filter = workflows.filter_tags(only=['/v.*/']) + workflows.filter_bran local branches_filter = workflows.filter_branches(only=['/.*/']) + workflows.filter_tags(ignore=['/.*/']); local homedir = '/home/circleci/banshee'; -local gover = '1.21'; +local gover = '1.22'; pipeline.new( @@ -33,8 +33,6 @@ pipeline.new( failfast: true, race: true, }}, - steps.run('go get github.com/golangci/golangci-lint/cmd/golangci-lint'), - steps.run('go run github.com/golangci/golangci-lint/cmd/golangci-lint run ./...'), ], ) ], diff --git a/docs/migrations.md b/docs/migrations.md index a116356..0e36068 100644 --- a/docs/migrations.md +++ b/docs/migrations.md @@ -5,8 +5,9 @@ - [Overview](#overview) - [Actions](#actions) - - [Find and replace](#find-and-replace) - - [Run commands](#run-commands) + - [Find and replace](#find-and-replace) + - [Run commands](#run-commands) + - [YAML](#yaml) @@ -28,11 +29,11 @@ Any field with a default of `-` is a required field. ## Find and replace -| Key | Description | Default | -|-----: |----------------------------------------------------- |--------- | -| old | Old string to be replaced | – | -| new | New string to replace it with | – | -| glob | The glob pattern for file matching the replacements | "**" | +| Key | Description | Default | +| ---: | --------------------------------------------------- | ------- | +| old | Old string to be replaced | – | +| new | New string to replace it with | – | +| glob | The glob pattern for file matching the replacements | "**" | ```yaml @@ -46,9 +47,9 @@ Any field with a default of `-` is a required field. ## Run commands -| Key | Description | Default | -|--------: |------------------------------------------------------------------------------------------------- |--------- | -| command | The command to be run. This command is passed to a bash shell, so it should be bash compatible. | – | +| Key | Description | Default | +| ------: | ----------------------------------------------------------------------------------------------- | ------- | +| command | The command to be run. This command is passed to a bash shell, so it should be bash compatible. | – | ```yaml - action: run_command @@ -56,3 +57,44 @@ Any field with a default of `-` is a required field. input: command: "echo 'Test' > test.txt" ``` + +## YAML + +A helper for making YAML file changes. + +| Key | Description | Default | +| ---------: | ------------------------------------------------------------------- | ------- | +| glob | The glob pattern for file matching the replacements | – | +| yamlpath | A dot notation path to the key being updated/added/deleted | – | +| sub_action | The YAML action being performed (replace, add, delete, list_append) | – | +| value | The value to be added | – | + +```yaml +- action: yaml + description: "Change a YAML file" + input: + glob: "example.yaml" + sub_action: replace + yamlpath: "firstlevel.secondlevel" + value: "new value" +- action: yaml + description: "Change a YAML file" + input: + glob: "example.yaml" + sub_action: add + yamlpath: "firstlevel.secondlevel" + value: "new value" +- action: yaml + description: "Change a YAML file" + input: + glob: "example.yaml" + sub_action: delete + yamlpath: "firstlevel.secondlevel" +- action: yaml + description: "Change a YAML file" + input: + glob: "example.yaml" + sub_action: list_append + yamlpath: "firstlevel.secondlevel" + value: "new item" +``` \ No newline at end of file diff --git a/examples/migration_config/migration.yaml b/examples/migration_config/migration.yaml index 1159e29..819ebc5 100644 --- a/examples/migration_config/migration.yaml +++ b/examples/migration_config/migration.yaml @@ -17,12 +17,40 @@ actions: - action: replace description: "This is an example of a replacement" input: + glob: "*.md" # Default is "**" for every file old: example string to replace new: this string is going to be better - action: run_command description: "Example command run" input: command: "echo 'Test' > test.txt" + - action: yaml + description: "Change a YAML file" + input: + glob: "example.yaml" + sub_action: replace + yamlpath: "firstlevel.secondlevel" + value: "new value" + - action: yaml + description: "Change a YAML file" + input: + glob: "example.yaml" + sub_action: add + yamlpath: "firstlevel.secondlevel" + value: "new value" + - action: yaml + description: "Change a YAML file" + input: + glob: "example.yaml" + sub_action: delete + yamlpath: "firstlevel.secondlevel" + - action: yaml + description: "Change a YAML file" + input: + glob: "example.yaml" + sub_action: list_append + yamlpath: "firstlevel.secondlevel" + value: "new item" pr_title: "An example PR title" pr_body_file: "examples/prbody.md" diff --git a/go.mod b/go.mod index f0b0fdf..336ba77 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/thejokersthief/banshee -go 1.21 +go 1.22.1 -toolchain go1.21.4 +toolchain go1.22.6 require ( github.com/alecthomas/kong v0.9.0 @@ -10,6 +10,7 @@ require ( github.com/bradleyfalzon/ghinstallation/v2 v2.11.0 github.com/charmbracelet/lipgloss v0.12.1 github.com/go-git/go-git/v5 v5.12.0 + github.com/goccy/go-yaml v1.12.0 github.com/google/go-github v17.0.0+incompatible github.com/google/go-github/v63 v63.0.0 github.com/gosimple/slug v1.14.0 @@ -31,6 +32,7 @@ require ( github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect + github.com/fatih/color v1.17.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect @@ -41,6 +43,7 @@ require ( github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -54,13 +57,14 @@ require ( github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/skeema/knownhosts v1.2.2 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - golang.org/x/crypto v0.23.0 // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.25.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/text v0.15.0 // indirect - golang.org/x/tools v0.21.0 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/mod v0.20.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect + golang.org/x/tools v0.24.0 // indirect + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 84516a3..ebe19a7 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcej github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= @@ -48,6 +50,14 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/goccy/go-yaml v1.12.0 h1:/1WHjnMsI1dlIBQutrvSMGZRQufVO3asrHfTwfACoPM= +github.com/goccy/go-yaml v1.12.0/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= @@ -85,8 +95,13 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= @@ -141,12 +156,12 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -155,15 +170,15 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -173,19 +188,20 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -193,17 +209,19 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= -golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/pkg/actions/actions.go b/pkg/actions/actions.go index b76e1b4..52e8cf3 100644 --- a/pkg/actions/actions.go +++ b/pkg/actions/actions.go @@ -24,8 +24,10 @@ func RunAction(log *logrus.Entry, globalConfig *configs.GlobalConfig, actionID s action = NewReplaceAction(dir, description, input, globalConfig.Options.IgnoreDirectories) case "run_command": action = NewRunCommandAction(dir, description, input) + case "yaml": + action = NewYAMLAction(dir, description, input) default: - return fmt.Errorf("Unrecognised command: %s", actionID) + return fmt.Errorf("unrecognised command: %s", actionID) } return action.Run(actionLog) diff --git a/pkg/actions/replace_test.go b/pkg/actions/replace_test.go new file mode 100644 index 0000000..9885675 --- /dev/null +++ b/pkg/actions/replace_test.go @@ -0,0 +1,64 @@ +package actions + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +func TestReplace_findAndReplaceWorker(t *testing.T) { + // Create a temporary directory for testing + tempDir := t.TempDir() + + // Create a temporary file with the old string + oldFile := filepath.Join(tempDir, "old.txt") + err := os.WriteFile(oldFile, []byte("This is the old string"), 0644) + assert.NoError(t, err) + + // Create a temporary file without the old string + newFile := filepath.Join(tempDir, "new.txt") + err = os.WriteFile(newFile, []byte("This is a new file"), 0644) + assert.NoError(t, err) + + // Create a Replace instance + r := &Replace{ + OldString: "old string", + NewString: "new string", + } + + // Create a logrus logger + logger := logrus.New() + + // Create channels for files and errors + files := make(chan string) + errChan := make(chan error) + + // Start the worker in a goroutine + go r.findAndReplaceWorker(logger.WithField("action", "replace"), files, errChan) + + // Send the files to the worker + files <- oldFile + files <- newFile + close(files) + + // Wait for the worker to finish + if len(errChan) != 0 { + for err := range errChan { + assert.NoError(t, err) + } + } + + // Check if the old string was replaced in the old file + content, err := os.ReadFile(oldFile) + assert.NoError(t, err) + assert.True(t, strings.Contains(string(content), "new string")) + + // Check if the new file remains unchanged + content, err = os.ReadFile(newFile) + assert.NoError(t, err) + assert.False(t, strings.Contains(string(content), "new string")) +} diff --git a/pkg/actions/run_command.go b/pkg/actions/run_command.go index 8a770ae..b8b2cf1 100644 --- a/pkg/actions/run_command.go +++ b/pkg/actions/run_command.go @@ -30,7 +30,7 @@ func (r *RunCommand) Run(log *logrus.Entry) error { cmd.Stderr = log.WriterLevel(logrus.ErrorLevel) cmd.Dir = r.BaseDir if err := cmd.Run(); err != nil { - return fmt.Errorf("Failed running `%s`: %w", r.Command, err) + return fmt.Errorf("failed running `%s`: %w", r.Command, err) } return nil diff --git a/pkg/actions/run_command_test.go b/pkg/actions/run_command_test.go new file mode 100644 index 0000000..7a5ac8e --- /dev/null +++ b/pkg/actions/run_command_test.go @@ -0,0 +1,30 @@ +package actions + +import ( + "bytes" + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +func TestRunCommand_Run(t *testing.T) { + // Create a new RunCommand instance + rc := &RunCommand{ + Command: "echo 'Hello, World!'", + BaseDir: "./", + } + + // Create a buffer to capture the command output + var buf bytes.Buffer + logger := logrus.New() + logger.Out = &buf + + // Run the command + err := rc.Run(logger.WithField("action", "run_command")) + assert.NoError(t, err) + + // Check if the command output matches the expected value + expectedOutput := "" + assert.Equal(t, expectedOutput, buf.String()) +} diff --git a/pkg/actions/yaml.go b/pkg/actions/yaml.go new file mode 100644 index 0000000..8103546 --- /dev/null +++ b/pkg/actions/yaml.go @@ -0,0 +1,126 @@ +// Manipulate a YAML document +package actions + +import ( + "fmt" + "os" + "strings" + + "github.com/goccy/go-yaml" + "github.com/sirupsen/logrus" + "github.com/yargevad/filepathx" +) + +type YAML struct { + SubAction string + Old string + New string + Glob string +} + +func NewYAMLAction(dir string, description string, input map[string]string) *YAML { + glob, hasSpecifiedGlob := input["glob"] + if !hasSpecifiedGlob { + glob = "**/*.yaml" + } + globPattern := dir + "/" + glob + + return &YAML{ + SubAction: input["sub_action"], + Old: input["yamlpath"], + New: input["value"], + Glob: globPattern, + } +} + +// Run executes the YAML action. +// It searches for files that match the specified glob pattern, +// reads each file, unmarshals its content into a YAML document, +// performs the specified subaction (e.g., delete), and then +// writes the modified document back to the file. +// If any errors occur during the process, they are logged and +// the execution continues with the next file. +func (r *YAML) Run(log *logrus.Entry) error { + + matches, err := filepathx.Glob(r.Glob) + if err != nil { + logrus.WithField("pattern", r.Glob).Error("Error globbing file path: ", err) + return err + } + + for _, file := range matches { + var doc map[string]interface{} + cm := yaml.CommentMap{} + + content, readErr := os.ReadFile(file) + if readErr != nil { + log.Errorf("error changing %s: %s", file, readErr) + continue + } + + var err error + if err = yaml.UnmarshalWithOptions(content, &doc, yaml.CommentToMap(cm)); err != nil { + return err + } + + parent, lastKey, traversalErr := r.traverseViaDotNotation(&doc, r.Old) + if traversalErr != nil { + log.Errorf("error changing %s: %s", file, traversalErr) + continue + } + + switch r.SubAction { + case "delete": + delete(*parent, lastKey) + case "replace": + (*parent)[lastKey] = r.New + case "add": + (*parent)[lastKey] = r.New + case "list_append": + if parentList, ok := (*parent)[lastKey].([]interface{}); ok { + (*parent)[lastKey] = append(parentList, r.New) + } + default: + // If the subaction is unknown, we won't proveed any further + return fmt.Errorf("unknown sub action: %s", r.SubAction) + } + + out, marhsalErr := yaml.MarshalWithOptions(doc, yaml.WithComment(cm)) + if marhsalErr != nil { + return marhsalErr + } + + writeErr := os.WriteFile(file, out, 0644) + if writeErr != nil { + log.Errorf("error changing %s: %s", file, writeErr) + continue + } + } + + return nil +} + +// traverseViaDotNotation traverses a YAML data structure using dot notation. +// It takes a pointer to an anonymous YAML struc and a path string in dot notation. +// It returns a pointer to the local parent in the YAML doc, the last key in the dot notation, and an error if any. +func (r *YAML) traverseViaDotNotation(data *map[string]interface{}, path string) (*map[string]interface{}, string, error) { + keys := strings.Split(path, ".") + + lastKey := keys[len(keys)-1] + parentKeys := keys[:len(keys)-1] + + parent := *data + for _, k := range parentKeys { + value, ok := parent[k] + if !ok { + return nil, "", fmt.Errorf("path not found: %s", path) + } + + parent, ok = value.(map[string]interface{}) + if !ok { + return nil, "", fmt.Errorf("invalid key: %s", path) + } + } + + return &parent, lastKey, nil +} diff --git a/pkg/actions/yaml_test.go b/pkg/actions/yaml_test.go new file mode 100644 index 0000000..e912612 --- /dev/null +++ b/pkg/actions/yaml_test.go @@ -0,0 +1,139 @@ +package actions + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +const yamlFileContent = ` +key1: value1 +# Comment +key2: + subkey: value2 + arrayList: + - item1 + - item2 +` + +func TestYAML_Run_Delete(t *testing.T) { + y, logEntry, yamlFile := test_setup(t) + + // Run the YAML action + err := y.Run(logEntry) + assert.NoError(t, err) + + // Check if the key was deleted from the YAML file + content, err := os.ReadFile(yamlFile) + assert.NoError(t, err) + expectedOutput := `# Comment +key2: + arrayList: + - item1 + - item2 + subkey: value2 +` + assert.Equal(t, expectedOutput, string(content)) +} + +func TestYAML_Run_Add(t *testing.T) { + y, logEntry, yamlFile := test_setup(t) + y.SubAction = "add" + y.Old = "key2.newkey" + y.New = "new value" + + // Run the YAML action + err := y.Run(logEntry) + assert.NoError(t, err) + + // Check if the key was deleted from the YAML file + content, err := os.ReadFile(yamlFile) + assert.NoError(t, err) + expectedOutput := `key1: value1 +# Comment +key2: + arrayList: + - item1 + - item2 + newkey: new value + subkey: value2 +` + assert.Equal(t, expectedOutput, string(content)) +} + +func TestYAML_Run_Replace(t *testing.T) { + y, logEntry, yamlFile := test_setup(t) + y.SubAction = "add" + y.Old = "key2.subkey" + y.New = "new value" + + // Run the YAML action + err := y.Run(logEntry) + assert.NoError(t, err) + + // Check if the key was deleted from the YAML file + content, err := os.ReadFile(yamlFile) + assert.NoError(t, err) + expectedOutput := `key1: value1 +# Comment +key2: + arrayList: + - item1 + - item2 + subkey: new value +` + assert.Equal(t, expectedOutput, string(content)) +} + +func TestYAML_Run_ListAppend(t *testing.T) { + y, logEntry, yamlFile := test_setup(t) + y.SubAction = "list_append" + y.Old = "key2.arrayList" + y.New = "item3" + + // Run the YAML action + err := y.Run(logEntry) + assert.NoError(t, err) + + // Check if the key was deleted from the YAML file + content, err := os.ReadFile(yamlFile) + assert.NoError(t, err) + expectedOutput := `key1: value1 +# Comment +key2: + arrayList: + - item1 + - item2 + - item3 + subkey: value2 +` + assert.Equal(t, expectedOutput, string(content)) +} + +func test_setup(t *testing.T) (*YAML, *logrus.Entry, string) { + // Create a temporary directory for testing + tempDir := t.TempDir() + + // Create a temporary YAML file + yamlFile := filepath.Join(tempDir, "test.yaml") + err := os.WriteFile(yamlFile, []byte(yamlFileContent), 0644) + assert.NoError(t, err) + + // Create a new YAML instance + y := &YAML{ + Glob: filepath.Join(tempDir, "*.yaml"), + Old: "key1", + SubAction: "delete", + } + + // Create a buffer to capture the log output + var buf bytes.Buffer + logger := logrus.New() + logger.Out = &buf + + return y, logger.WithField("action", "yaml"), yamlFile +} diff --git a/schemas/migration.json b/schemas/migration.json index 722b954..40a6ac8 100644 --- a/schemas/migration.json +++ b/schemas/migration.json @@ -61,7 +61,17 @@ "type": "string" }, "input": { - "$ref": "#/definitions/Input" + "oneOf": [ + { + "$ref": "#/definitions/ReplaceInput" + }, + { + "$ref": "#/definitions/RunCommandInput" + }, + { + "$ref": "#/definitions/YAMLInput" + } + ] } }, "required": [ @@ -71,7 +81,7 @@ ], "title": "Action" }, - "Input": { + "ReplaceInput": { "type": "object", "additionalProperties": false, "properties": { @@ -81,12 +91,43 @@ "new": { "type": "string" }, + "glob": { + "type": "string" + } + }, + "required": ["old", "new"], + "title": "ReplaceInput" + }, + "RunCommandInput": { + "type": "object", + "additionalProperties": false, + "properties": { "command": { "type": "string" } }, - "required": [], - "title": "Input" + "required": ["command"], + "title": "RunCommandInput" + }, + "YAMLInput": { + "type": "object", + "additionalProperties": false, + "properties": { + "glob": { + "type": "string" + }, + "sub_action": { + "type": "string" + }, + "yamlpath": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": ["glob", "sub_action", "yamlpath"], + "title": "YAMLInput" } } }