Refactor expression evaluator to use parser from actionlint package (#908)
* feat: implement expression evaluator Co-authored-by: Markus Wolf <markus.wolf@new-work.se> Co-authored-by: Philipp Hinrichsen <philipp.hinrichsen@new-work.se> * feat: integrate exprparser into act Co-authored-by: Markus Wolf <markus.wolf@new-work.se> Co-authored-by: Philipp Hinrichsen <philipp.hinrichsen@new-work.se> * Escape { and }, do not fail on missing properties * Fix empty inputs context * fix: contains() comparison for complex values Co-authored-by: Markus Wolf <markus.wolf@new-work.se> Co-authored-by: Philipp Hinrichsen <philipp.hinrichsen@new-work.se> Co-authored-by: Christopher Homberger <christopher.homberger@web.de>
This commit is contained in:
parent
518148d162
commit
eba71f98fe
5
go.mod
5
go.mod
|
@ -22,7 +22,6 @@ require (
|
||||||
github.com/julienschmidt/httprouter v1.3.0
|
github.com/julienschmidt/httprouter v1.3.0
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||||
github.com/kevinburke/ssh_config v1.1.0 // indirect
|
github.com/kevinburke/ssh_config v1.1.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.9 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.14
|
github.com/mattn/go-isatty v0.0.14
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||||
github.com/mitchellh/go-homedir v1.1.0
|
github.com/mitchellh/go-homedir v1.1.0
|
||||||
|
@ -31,7 +30,7 @@ require (
|
||||||
github.com/opencontainers/runc v1.0.2 // indirect
|
github.com/opencontainers/runc v1.0.2 // indirect
|
||||||
github.com/opencontainers/selinux v1.10.0
|
github.com/opencontainers/selinux v1.10.0
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/robertkrimen/otto v0.0.0-20210614181706-373ff5438452
|
github.com/rhysd/actionlint v1.6.8
|
||||||
github.com/sabhiram/go-gitignore v0.0.0-20201211210132-54b8a0bf510f
|
github.com/sabhiram/go-gitignore v0.0.0-20201211210132-54b8a0bf510f
|
||||||
github.com/sergi/go-diff v1.2.0 // indirect
|
github.com/sergi/go-diff v1.2.0 // indirect
|
||||||
github.com/sirupsen/logrus v1.8.1
|
github.com/sirupsen/logrus v1.8.1
|
||||||
|
@ -41,10 +40,8 @@ require (
|
||||||
github.com/xanzy/ssh-agent v0.3.1 // indirect
|
github.com/xanzy/ssh-agent v0.3.1 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
|
||||||
golang.org/x/net v0.0.0-20210917221730-978cfadd31cf // indirect
|
golang.org/x/net v0.0.0-20210917221730-978cfadd31cf // indirect
|
||||||
golang.org/x/sys v0.0.0-20210923061019-b8560ed6a9b7 // indirect
|
|
||||||
golang.org/x/term v0.0.0-20210916214954-140adaaadfaf
|
golang.org/x/term v0.0.0-20210916214954-140adaaadfaf
|
||||||
golang.org/x/text v0.3.7 // indirect
|
golang.org/x/text v0.3.7 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20210921142501-181ce0d877f6 // indirect
|
google.golang.org/genproto v0.0.0-20210921142501-181ce0d877f6 // indirect
|
||||||
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||||
)
|
)
|
||||||
|
|
25
go.sum
25
go.sum
|
@ -425,6 +425,7 @@ github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZ
|
||||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
|
github.com/dvyukov/go-fuzz v0.0.0-20210914135545-4980593459a1/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||||
|
@ -446,6 +447,8 @@ github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi
|
||||||
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||||
|
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||||
|
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||||
github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
||||||
|
@ -825,8 +828,9 @@ github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcncea
|
||||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
|
|
||||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
|
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
|
||||||
|
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||||
github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=
|
github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=
|
||||||
github.com/mattn/go-ieproxy v0.0.0-20190702010315-6dee0af9227d/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=
|
github.com/mattn/go-ieproxy v0.0.0-20190702010315-6dee0af9227d/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=
|
||||||
github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E=
|
github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E=
|
||||||
|
@ -839,6 +843,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
|
||||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||||
|
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||||
|
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
||||||
github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
|
@ -1027,8 +1033,12 @@ github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:
|
||||||
github.com/quasilyte/go-ruleguard v0.1.2-0.20200318202121-b00d7a75d3d8/go.mod h1:CGFX09Ci3pq9QZdj86B+VGIdNj4VyCo2iPOGS9esB/k=
|
github.com/quasilyte/go-ruleguard v0.1.2-0.20200318202121-b00d7a75d3d8/go.mod h1:CGFX09Ci3pq9QZdj86B+VGIdNj4VyCo2iPOGS9esB/k=
|
||||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
|
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
|
||||||
github.com/robertkrimen/otto v0.0.0-20210614181706-373ff5438452 h1:ewTtJ72GFy2e0e8uyiDwMG3pKCS5mBh+hdSTYsPKEP8=
|
github.com/rhysd/actionlint v1.6.8 h1:li0691FNuuS3da2igfjMb9M58AgMXX7j9U5EgbCZFuc=
|
||||||
github.com/robertkrimen/otto v0.0.0-20210614181706-373ff5438452/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY=
|
github.com/rhysd/actionlint v1.6.8/go.mod h1:0AA4pvZ2nrZHT6D86eUhieH2NFmLqhxrNex0NEa2A2g=
|
||||||
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
|
||||||
|
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
|
||||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||||
github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||||
|
@ -1189,6 +1199,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
|
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
|
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
|
||||||
github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=
|
github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=
|
||||||
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
|
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
|
||||||
|
@ -1395,6 +1406,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
|
||||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
@ -1494,8 +1506,9 @@ golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210923061019-b8560ed6a9b7 h1:c20P3CcPbopVp2f7099WLOqSNKURf30Z0uq66HpijZY=
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210923061019-b8560ed6a9b7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211113001501-0c823b97ae02 h1:7NCfEGl0sfUojmX78nK9pBJuUlSZWEJA/TwASvfiPLo=
|
||||||
|
golang.org/x/sys v0.0.0-20211113001501-0c823b97ae02/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
|
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
|
||||||
|
@ -1767,8 +1780,6 @@ gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||||
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
|
|
||||||
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
|
|
||||||
gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||||
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||||
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||||
|
|
|
@ -0,0 +1,277 @@
|
||||||
|
package exprparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/nektos/act/pkg/model"
|
||||||
|
"github.com/rhysd/actionlint"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (impl *interperterImpl) contains(search, item reflect.Value) (bool, error) {
|
||||||
|
switch search.Kind() {
|
||||||
|
case reflect.String, reflect.Int, reflect.Float64, reflect.Bool, reflect.Invalid:
|
||||||
|
return strings.Contains(
|
||||||
|
strings.ToLower(impl.coerceToString(search).String()),
|
||||||
|
strings.ToLower(impl.coerceToString(item).String()),
|
||||||
|
), nil
|
||||||
|
|
||||||
|
case reflect.Slice:
|
||||||
|
for i := 0; i < search.Len(); i++ {
|
||||||
|
arrayItem := search.Index(i).Elem()
|
||||||
|
result, err := impl.compareValues(arrayItem, item, actionlint.CompareOpNodeKindEq)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if isEqual, ok := result.(bool); ok && isEqual {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) startsWith(searchString, searchValue reflect.Value) (bool, error) {
|
||||||
|
return strings.HasPrefix(
|
||||||
|
strings.ToLower(impl.coerceToString(searchString).String()),
|
||||||
|
strings.ToLower(impl.coerceToString(searchValue).String()),
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) endsWith(searchString, searchValue reflect.Value) (bool, error) {
|
||||||
|
return strings.HasSuffix(
|
||||||
|
strings.ToLower(impl.coerceToString(searchString).String()),
|
||||||
|
strings.ToLower(impl.coerceToString(searchValue).String()),
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
passThrough = iota
|
||||||
|
bracketOpen
|
||||||
|
bracketClose
|
||||||
|
)
|
||||||
|
|
||||||
|
func (impl *interperterImpl) format(str reflect.Value, replaceValue ...reflect.Value) (string, error) {
|
||||||
|
input := impl.coerceToString(str).String()
|
||||||
|
output := ""
|
||||||
|
replacementIndex := ""
|
||||||
|
|
||||||
|
state := passThrough
|
||||||
|
for _, character := range input {
|
||||||
|
switch state {
|
||||||
|
case passThrough: // normal buffer output
|
||||||
|
switch character {
|
||||||
|
case '{':
|
||||||
|
state = bracketOpen
|
||||||
|
|
||||||
|
case '}':
|
||||||
|
state = bracketClose
|
||||||
|
|
||||||
|
default:
|
||||||
|
output += string(character)
|
||||||
|
}
|
||||||
|
|
||||||
|
case bracketOpen: // found {
|
||||||
|
switch character {
|
||||||
|
case '{':
|
||||||
|
output += "{"
|
||||||
|
replacementIndex = ""
|
||||||
|
state = passThrough
|
||||||
|
|
||||||
|
case '}':
|
||||||
|
index, err := strconv.ParseInt(replacementIndex, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("The following format string is invalid: '%s'", input)
|
||||||
|
}
|
||||||
|
|
||||||
|
replacementIndex = ""
|
||||||
|
|
||||||
|
if len(replaceValue) <= int(index) {
|
||||||
|
return "", fmt.Errorf("The following format string references more arguments than were supplied: '%s'", input)
|
||||||
|
}
|
||||||
|
|
||||||
|
output += impl.coerceToString(replaceValue[index]).String()
|
||||||
|
|
||||||
|
state = passThrough
|
||||||
|
|
||||||
|
default:
|
||||||
|
replacementIndex += string(character)
|
||||||
|
}
|
||||||
|
|
||||||
|
case bracketClose: // found }
|
||||||
|
switch character {
|
||||||
|
case '}':
|
||||||
|
output += "}"
|
||||||
|
replacementIndex = ""
|
||||||
|
state = passThrough
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic("Invalid format parser state")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if state != passThrough {
|
||||||
|
switch state {
|
||||||
|
case bracketOpen:
|
||||||
|
return "", fmt.Errorf("Unclosed brackets. The following format string is invalid: '%s'", input)
|
||||||
|
|
||||||
|
case bracketClose:
|
||||||
|
return "", fmt.Errorf("Closing bracket without opening one. The following format string is invalid: '%s'", input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) join(array reflect.Value, sep reflect.Value) (string, error) {
|
||||||
|
separator := impl.coerceToString(sep).String()
|
||||||
|
switch array.Kind() {
|
||||||
|
case reflect.Slice:
|
||||||
|
var items []string
|
||||||
|
for i := 0; i < array.Len(); i++ {
|
||||||
|
items = append(items, impl.coerceToString(array.Index(i).Elem()).String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(items, separator), nil
|
||||||
|
default:
|
||||||
|
return strings.Join([]string{impl.coerceToString(array).String()}, separator), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) toJSON(value reflect.Value) (string, error) {
|
||||||
|
if value.Kind() == reflect.Invalid {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
json, err := json.MarshalIndent(value.Interface(), "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Cannot convert value to JSON. Cause: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(json), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) fromJSON(value reflect.Value) (interface{}, error) {
|
||||||
|
if value.Kind() != reflect.String {
|
||||||
|
return nil, fmt.Errorf("Cannot parse non-string type %v as JSON", value.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
var data interface{}
|
||||||
|
|
||||||
|
err := json.Unmarshal([]byte(value.String()), &data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Invalid JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) hashFiles(paths ...reflect.Value) (string, error) {
|
||||||
|
var filepaths []string
|
||||||
|
|
||||||
|
for _, path := range paths {
|
||||||
|
if path.Kind() == reflect.String {
|
||||||
|
filepaths = append(filepaths, path.String())
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf("Non-string path passed to hashFiles")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var files []string
|
||||||
|
|
||||||
|
for i := range filepaths {
|
||||||
|
newFiles, err := filepath.Glob(filepath.Join(impl.config.WorkingDir, filepaths[i]))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Unable to glob.Glob: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
files = append(files, newFiles...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(files) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hasher := sha256.New()
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
f, err := os.Open(file)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Unable to os.Open: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.Copy(hasher, f); err != nil {
|
||||||
|
return "", fmt.Errorf("Unable to io.Copy: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
return "", fmt.Errorf("Unable to Close file: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hex.EncodeToString(hasher.Sum(nil)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) getNeedsTransitive(job *model.Job) []string {
|
||||||
|
needs := job.Needs()
|
||||||
|
|
||||||
|
for _, need := range needs {
|
||||||
|
parentNeeds := impl.getNeedsTransitive(impl.config.Run.Workflow.GetJob(need))
|
||||||
|
needs = append(needs, parentNeeds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return needs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) always() (bool, error) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) jobSuccess() (bool, error) {
|
||||||
|
jobs := impl.config.Run.Workflow.Jobs
|
||||||
|
jobNeeds := impl.getNeedsTransitive(impl.config.Run.Job())
|
||||||
|
|
||||||
|
for _, needs := range jobNeeds {
|
||||||
|
if jobs[needs].Result != "success" {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) stepSuccess() (bool, error) {
|
||||||
|
return impl.env.Job.Status == "success", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) jobFailure() (bool, error) {
|
||||||
|
jobs := impl.config.Run.Workflow.Jobs
|
||||||
|
jobNeeds := impl.getNeedsTransitive(impl.config.Run.Job())
|
||||||
|
|
||||||
|
for _, needs := range jobNeeds {
|
||||||
|
if jobs[needs].Result == "failure" {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) stepFailure() (bool, error) {
|
||||||
|
return impl.env.Job.Status == "failure", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) cancelled() (bool, error) {
|
||||||
|
return impl.env.Job.Status == "cancelled", nil
|
||||||
|
}
|
|
@ -0,0 +1,245 @@
|
||||||
|
package exprparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nektos/act/pkg/model"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFunctionContains(t *testing.T) {
|
||||||
|
table := []struct {
|
||||||
|
input string
|
||||||
|
expected interface{}
|
||||||
|
name string
|
||||||
|
}{
|
||||||
|
{"contains('search', 'item') }}", false, "contains-str-str"},
|
||||||
|
{`cOnTaInS('Hello', 'll') }}`, true, "contains-str-casing"},
|
||||||
|
{`contains('HELLO', 'll') }}`, true, "contains-str-casing"},
|
||||||
|
{`contains('3.141592', 3.14) }}`, true, "contains-str-number"},
|
||||||
|
{`contains(3.141592, '3.14') }}`, true, "contains-number-str"},
|
||||||
|
{`contains(3.141592, 3.14) }}`, true, "contains-number-number"},
|
||||||
|
{`contains(true, 'u') }}`, true, "contains-bool-str"},
|
||||||
|
{`contains(null, '') }}`, true, "contains-null-str"},
|
||||||
|
{`contains(fromJSON('["first","second"]'), 'first') }}`, true, "contains-item"},
|
||||||
|
{`contains(fromJSON('[null,"second"]'), '') }}`, true, "contains-item-null-empty-str"},
|
||||||
|
{`contains(fromJSON('["","second"]'), null) }}`, true, "contains-item-empty-str-null"},
|
||||||
|
{`contains(fromJSON('[true,"second"]'), 'true') }}`, false, "contains-item-bool-arr"},
|
||||||
|
{`contains(fromJSON('["true","second"]'), true) }}`, false, "contains-item-str-bool"},
|
||||||
|
{`contains(fromJSON('[3.14,"second"]'), '3.14') }}`, true, "contains-item-number-str"},
|
||||||
|
{`contains(fromJSON('[3.14,"second"]'), 3.14) }}`, true, "contains-item-number-number"},
|
||||||
|
{`contains(fromJSON('["","second"]'), fromJSON('[]')) }}`, false, "contains-item-str-arr"},
|
||||||
|
{`contains(fromJSON('["","second"]'), fromJSON('{}')) }}`, false, "contains-item-str-obj"},
|
||||||
|
}
|
||||||
|
|
||||||
|
env := &EvaluationEnvironment{}
|
||||||
|
|
||||||
|
for _, tt := range table {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, false)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expected, output)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFunctionStartsWith(t *testing.T) {
|
||||||
|
table := []struct {
|
||||||
|
input string
|
||||||
|
expected interface{}
|
||||||
|
name string
|
||||||
|
}{
|
||||||
|
{"startsWith('search', 'se') }}", true, "startswith-string"},
|
||||||
|
{"startsWith('search', 'sa') }}", false, "startswith-string"},
|
||||||
|
{"startsWith('123search', '123s') }}", true, "startswith-string"},
|
||||||
|
{"startsWith(123, 's') }}", false, "startswith-string"},
|
||||||
|
{"startsWith(123, '12') }}", true, "startswith-string"},
|
||||||
|
{"startsWith('123', 12) }}", true, "startswith-string"},
|
||||||
|
{"startsWith(null, '42') }}", false, "startswith-string"},
|
||||||
|
{"startsWith('null', null) }}", true, "startswith-string"},
|
||||||
|
{"startsWith('null', '') }}", true, "startswith-string"},
|
||||||
|
}
|
||||||
|
|
||||||
|
env := &EvaluationEnvironment{}
|
||||||
|
|
||||||
|
for _, tt := range table {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, false)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expected, output)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFunctionEndsWith(t *testing.T) {
|
||||||
|
table := []struct {
|
||||||
|
input string
|
||||||
|
expected interface{}
|
||||||
|
name string
|
||||||
|
}{
|
||||||
|
{"endsWith('search', 'ch') }}", true, "endsWith-string"},
|
||||||
|
{"endsWith('search', 'sa') }}", false, "endsWith-string"},
|
||||||
|
{"endsWith('search123s', '123s') }}", true, "endsWith-string"},
|
||||||
|
{"endsWith(123, 's') }}", false, "endsWith-string"},
|
||||||
|
{"endsWith(123, '23') }}", true, "endsWith-string"},
|
||||||
|
{"endsWith('123', 23) }}", true, "endsWith-string"},
|
||||||
|
{"endsWith(null, '42') }}", false, "endsWith-string"},
|
||||||
|
{"endsWith('null', null) }}", true, "endsWith-string"},
|
||||||
|
{"endsWith('null', '') }}", true, "endsWith-string"},
|
||||||
|
}
|
||||||
|
|
||||||
|
env := &EvaluationEnvironment{}
|
||||||
|
|
||||||
|
for _, tt := range table {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, false)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expected, output)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFunctionJoin(t *testing.T) {
|
||||||
|
table := []struct {
|
||||||
|
input string
|
||||||
|
expected interface{}
|
||||||
|
name string
|
||||||
|
}{
|
||||||
|
{"join(fromJSON('[\"a\", \"b\"]'), ',')", "a,b", "join-arr"},
|
||||||
|
{"join('string', ',')", "string", "join-str"},
|
||||||
|
{"join(1, ',')", "1", "join-number"},
|
||||||
|
{"join(null, ',')", "", "join-number"},
|
||||||
|
{"join(fromJSON('[\"a\", \"b\", null]'), null)", "ab", "join-number"},
|
||||||
|
{"join(fromJSON('[\"a\", \"b\"]'))", "a,b", "join-number"},
|
||||||
|
{"join(fromJSON('[\"a\", \"b\", null]'), 1)", "a1b1", "join-number"},
|
||||||
|
}
|
||||||
|
|
||||||
|
env := &EvaluationEnvironment{}
|
||||||
|
|
||||||
|
for _, tt := range table {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, false)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expected, output)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFunctionToJSON(t *testing.T) {
|
||||||
|
table := []struct {
|
||||||
|
input string
|
||||||
|
expected interface{}
|
||||||
|
name string
|
||||||
|
}{
|
||||||
|
{"toJSON(env) }}", "{\n \"key\": \"value\"\n}", "toJSON"},
|
||||||
|
}
|
||||||
|
|
||||||
|
env := &EvaluationEnvironment{
|
||||||
|
Env: map[string]string{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range table {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, false)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expected, output)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFunctionFromJSON(t *testing.T) {
|
||||||
|
table := []struct {
|
||||||
|
input string
|
||||||
|
expected interface{}
|
||||||
|
name string
|
||||||
|
}{
|
||||||
|
{"fromJSON('{\"foo\":\"bar\"}') }}", map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
}, "fromJSON"},
|
||||||
|
}
|
||||||
|
|
||||||
|
env := &EvaluationEnvironment{}
|
||||||
|
|
||||||
|
for _, tt := range table {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, false)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expected, output)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFunctionHashFiles(t *testing.T) {
|
||||||
|
table := []struct {
|
||||||
|
input string
|
||||||
|
expected interface{}
|
||||||
|
name string
|
||||||
|
}{
|
||||||
|
{"hashFiles('**/non-extant-files') }}", "", "hash-non-existing-file"},
|
||||||
|
{"hashFiles('**/non-extant-files', '**/more-non-extant-files') }}", "", "hash-multiple-non-existing-files"},
|
||||||
|
{"hashFiles('./for-hashing-1.txt') }}", "66a045b452102c59d840ec097d59d9467e13a3f34f6494e539ffd32c1bb35f18", "hash-single-file"},
|
||||||
|
{"hashFiles('./for-hashing-*') }}", "8e5935e7e13368cd9688fe8f48a0955293676a021562582c7e848dafe13fb046", "hash-multiple-files"},
|
||||||
|
}
|
||||||
|
|
||||||
|
env := &EvaluationEnvironment{}
|
||||||
|
|
||||||
|
for _, tt := range table {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
workdir, err := filepath.Abs("testdata")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
output, err := NewInterpeter(env, Config{WorkingDir: workdir}).Evaluate(tt.input, false)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expected, output)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFunctionFormat(t *testing.T) {
|
||||||
|
table := []struct {
|
||||||
|
input string
|
||||||
|
expected interface{}
|
||||||
|
error interface{}
|
||||||
|
name string
|
||||||
|
}{
|
||||||
|
{"format('text')", "text", nil, "format-plain-string"},
|
||||||
|
{"format('Hello {0} {1} {2}!', 'Mona', 'the', 'Octocat')", "Hello Mona the Octocat!", nil, "format-with-placeholders"},
|
||||||
|
{"format('{{Hello {0} {1} {2}!}}', 'Mona', 'the', 'Octocat')", "{Hello Mona the Octocat!}", nil, "format-with-escaped-braces"},
|
||||||
|
{"format('{{0}}', 'test')", "{0}", nil, "format-with-escaped-braces"},
|
||||||
|
{"format('{{{0}}}', 'test')", "{test}", nil, "format-with-escaped-braces-and-value"},
|
||||||
|
{"format('}}')", "}", nil, "format-output-closing-brace"},
|
||||||
|
{`format('Hello "{0}" {1} {2} {3} {4}', null, true, -3.14, NaN, Infinity)`, `Hello "" true -3.14 NaN Infinity`, nil, "format-with-primitives"},
|
||||||
|
{`format('Hello "{0}" {1} {2}', fromJSON('[0, true, "abc"]'), fromJSON('[{"a":1}]'), fromJSON('{"a":{"b":1}}'))`, `Hello "Array" Array Object`, nil, "format-with-complex-types"},
|
||||||
|
{"format(true)", "true", nil, "format-with-primitive-args"},
|
||||||
|
{"format('echo Hello {0} ${{Test}}', github.undefined_property)", "echo Hello ${Test}", nil, "format-with-undefined-value"},
|
||||||
|
{"format('{0}}', '{1}', 'World')", nil, "Closing bracket without opening one. The following format string is invalid: '{0}}'", "format-invalid-format-string"},
|
||||||
|
{"format('{0', '{1}', 'World')", nil, "Unclosed brackets. The following format string is invalid: '{0'", "format-invalid-format-string"},
|
||||||
|
{"format('{2}', '{1}', 'World')", "", "The following format string references more arguments than were supplied: '{2}'", "format-invalid-replacement-reference"},
|
||||||
|
{"format('{2147483648}')", "", "The following format string is invalid: '{2147483648}'", "format-invalid-replacement-reference"},
|
||||||
|
}
|
||||||
|
|
||||||
|
env := &EvaluationEnvironment{
|
||||||
|
Github: &model.GithubContext{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range table {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, false)
|
||||||
|
if tt.error != nil {
|
||||||
|
assert.Equal(t, tt.error, err.Error())
|
||||||
|
} else {
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, tt.expected, output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,576 @@
|
||||||
|
package exprparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/nektos/act/pkg/model"
|
||||||
|
"github.com/rhysd/actionlint"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EvaluationEnvironment struct {
|
||||||
|
Github *model.GithubContext
|
||||||
|
Env map[string]string
|
||||||
|
Job *model.JobContext
|
||||||
|
Steps map[string]*model.StepResult
|
||||||
|
Runner map[string]interface{}
|
||||||
|
Secrets map[string]string
|
||||||
|
Strategy map[string]interface{}
|
||||||
|
Matrix map[string]interface{}
|
||||||
|
Needs map[string]map[string]map[string]string
|
||||||
|
Inputs map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Run *model.Run
|
||||||
|
WorkingDir string
|
||||||
|
Context string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Interpreter interface {
|
||||||
|
Evaluate(input string, isIfExpression bool) (interface{}, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type interperterImpl struct {
|
||||||
|
env *EvaluationEnvironment
|
||||||
|
config Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInterpeter(env *EvaluationEnvironment, config Config) Interpreter {
|
||||||
|
return &interperterImpl{
|
||||||
|
env: env,
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) Evaluate(input string, isIfExpression bool) (interface{}, error) {
|
||||||
|
input = strings.TrimPrefix(input, "${{")
|
||||||
|
parser := actionlint.NewExprParser()
|
||||||
|
exprNode, err := parser.Parse(actionlint.NewExprLexer(input + "}}"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to parse: %s", err.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isIfExpression {
|
||||||
|
hasStatusCheckFunction := false
|
||||||
|
actionlint.VisitExprNode(exprNode, func(node, _ actionlint.ExprNode, entering bool) {
|
||||||
|
if funcCallNode, ok := node.(*actionlint.FuncCallNode); entering && ok {
|
||||||
|
switch strings.ToLower(funcCallNode.Callee) {
|
||||||
|
case "success", "always", "cancelled", "failure":
|
||||||
|
hasStatusCheckFunction = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if !hasStatusCheckFunction {
|
||||||
|
exprNode = &actionlint.LogicalOpNode{
|
||||||
|
Kind: actionlint.LogicalOpNodeKindAnd,
|
||||||
|
Left: &actionlint.FuncCallNode{
|
||||||
|
Callee: "success",
|
||||||
|
Args: []actionlint.ExprNode{},
|
||||||
|
},
|
||||||
|
Right: exprNode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err2 := impl.evaluateNode(exprNode)
|
||||||
|
|
||||||
|
return result, err2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) evaluateNode(exprNode actionlint.ExprNode) (interface{}, error) {
|
||||||
|
switch node := exprNode.(type) {
|
||||||
|
case *actionlint.VariableNode:
|
||||||
|
return impl.evaluateVariable(node)
|
||||||
|
case *actionlint.BoolNode:
|
||||||
|
return node.Value, nil
|
||||||
|
case *actionlint.NullNode:
|
||||||
|
return nil, nil
|
||||||
|
case *actionlint.IntNode:
|
||||||
|
return node.Value, nil
|
||||||
|
case *actionlint.FloatNode:
|
||||||
|
return node.Value, nil
|
||||||
|
case *actionlint.StringNode:
|
||||||
|
return node.Value, nil
|
||||||
|
case *actionlint.IndexAccessNode:
|
||||||
|
return impl.evaluateIndexAccess(node)
|
||||||
|
case *actionlint.ObjectDerefNode:
|
||||||
|
return impl.evaluateObjectDeref(node)
|
||||||
|
case *actionlint.ArrayDerefNode:
|
||||||
|
return impl.evaluateArrayDeref(node)
|
||||||
|
case *actionlint.NotOpNode:
|
||||||
|
return impl.evaluateNot(node)
|
||||||
|
case *actionlint.CompareOpNode:
|
||||||
|
return impl.evaluateCompare(node)
|
||||||
|
case *actionlint.LogicalOpNode:
|
||||||
|
return impl.evaluateLogicalCompare(node)
|
||||||
|
case *actionlint.FuncCallNode:
|
||||||
|
return impl.evaluateFuncCall(node)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("Fatal error! Unknown node type: %s node: %+v", reflect.TypeOf(exprNode), exprNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) evaluateVariable(variableNode *actionlint.VariableNode) (interface{}, error) {
|
||||||
|
switch strings.ToLower(variableNode.Name) {
|
||||||
|
case "github":
|
||||||
|
return impl.env.Github, nil
|
||||||
|
case "env":
|
||||||
|
return impl.env.Env, nil
|
||||||
|
case "job":
|
||||||
|
return impl.env.Job, nil
|
||||||
|
case "steps":
|
||||||
|
return impl.env.Steps, nil
|
||||||
|
case "runner":
|
||||||
|
return impl.env.Runner, nil
|
||||||
|
case "secrets":
|
||||||
|
return impl.env.Secrets, nil
|
||||||
|
case "strategy":
|
||||||
|
return impl.env.Strategy, nil
|
||||||
|
case "matrix":
|
||||||
|
return impl.env.Matrix, nil
|
||||||
|
case "needs":
|
||||||
|
return impl.env.Needs, nil
|
||||||
|
case "inputs":
|
||||||
|
return impl.env.Inputs, nil
|
||||||
|
case "infinity":
|
||||||
|
return math.Inf(1), nil
|
||||||
|
case "nan":
|
||||||
|
return math.NaN(), nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("Unavailable context: %s", variableNode.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) evaluateIndexAccess(indexAccessNode *actionlint.IndexAccessNode) (interface{}, error) {
|
||||||
|
left, err := impl.evaluateNode(indexAccessNode.Operand)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
leftValue := reflect.ValueOf(left)
|
||||||
|
|
||||||
|
right, err := impl.evaluateNode(indexAccessNode.Index)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rightValue := reflect.ValueOf(right)
|
||||||
|
|
||||||
|
switch rightValue.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
return impl.getPropertyValue(leftValue, rightValue.String())
|
||||||
|
|
||||||
|
case reflect.Int:
|
||||||
|
switch leftValue.Kind() {
|
||||||
|
case reflect.Slice:
|
||||||
|
return leftValue.Index(int(rightValue.Int())).Interface(), nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("Unable to index on non-slice value: %s", leftValue.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("Unknown index type: %s", rightValue.Kind())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) evaluateObjectDeref(objectDerefNode *actionlint.ObjectDerefNode) (interface{}, error) {
|
||||||
|
left, err := impl.evaluateNode(objectDerefNode.Receiver)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return impl.getPropertyValue(reflect.ValueOf(left), objectDerefNode.Property)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) evaluateArrayDeref(arrayDerefNode *actionlint.ArrayDerefNode) (interface{}, error) {
|
||||||
|
left, err := impl.evaluateNode(arrayDerefNode.Receiver)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return reflect.ValueOf(left).Interface(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) getPropertyValue(left reflect.Value, property string) (value interface{}, err error) {
|
||||||
|
switch left.Kind() {
|
||||||
|
case reflect.Ptr:
|
||||||
|
return impl.getPropertyValue(left.Elem(), property)
|
||||||
|
|
||||||
|
case reflect.Struct:
|
||||||
|
leftType := left.Type()
|
||||||
|
for i := 0; i < leftType.NumField(); i++ {
|
||||||
|
jsonName := leftType.Field(i).Tag.Get("json")
|
||||||
|
if jsonName == property {
|
||||||
|
property = leftType.Field(i).Name
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldValue := left.FieldByNameFunc(func(name string) bool {
|
||||||
|
return strings.EqualFold(name, property)
|
||||||
|
})
|
||||||
|
|
||||||
|
if fieldValue.Kind() == reflect.Invalid {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fieldValue.Interface(), nil
|
||||||
|
|
||||||
|
case reflect.Map:
|
||||||
|
iter := left.MapRange()
|
||||||
|
|
||||||
|
for iter.Next() {
|
||||||
|
key := iter.Key()
|
||||||
|
|
||||||
|
switch key.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
if strings.EqualFold(key.String(), property) {
|
||||||
|
return impl.getMapValue(iter.Value())
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("'%s' in map key not implemented", key.Kind())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
|
||||||
|
case reflect.Slice:
|
||||||
|
var values []interface{}
|
||||||
|
|
||||||
|
for i := 0; i < left.Len(); i++ {
|
||||||
|
value, err := impl.getPropertyValue(left.Index(i).Elem(), property)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
values = append(values, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return values, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("Unable to dereference '%s' on non-struct '%s'", property, left.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) getMapValue(value reflect.Value) (interface{}, error) {
|
||||||
|
if value.Kind() == reflect.Ptr {
|
||||||
|
return impl.getMapValue(value.Elem())
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.Interface(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) evaluateNot(notNode *actionlint.NotOpNode) (interface{}, error) {
|
||||||
|
operand, err := impl.evaluateNode(notNode.Operand)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return !impl.isTruthy(reflect.ValueOf(operand)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) evaluateCompare(compareNode *actionlint.CompareOpNode) (interface{}, error) {
|
||||||
|
left, err := impl.evaluateNode(compareNode.Left)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
right, err := impl.evaluateNode(compareNode.Right)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
leftValue := reflect.ValueOf(left)
|
||||||
|
rightValue := reflect.ValueOf(right)
|
||||||
|
|
||||||
|
return impl.compareValues(leftValue, rightValue, compareNode.Kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) compareValues(leftValue reflect.Value, rightValue reflect.Value, kind actionlint.CompareOpNodeKind) (interface{}, error) {
|
||||||
|
if leftValue.Kind() != rightValue.Kind() {
|
||||||
|
if !impl.isNumber(leftValue) {
|
||||||
|
leftValue = impl.coerceToNumber(leftValue)
|
||||||
|
}
|
||||||
|
if !impl.isNumber(rightValue) {
|
||||||
|
rightValue = impl.coerceToNumber(rightValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch leftValue.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
return impl.compareString(strings.ToLower(leftValue.String()), strings.ToLower(rightValue.String()), kind)
|
||||||
|
|
||||||
|
case reflect.Int:
|
||||||
|
if rightValue.Kind() == reflect.Float64 {
|
||||||
|
return impl.compareNumber(float64(leftValue.Int()), rightValue.Float(), kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
return impl.compareNumber(float64(leftValue.Int()), float64(rightValue.Int()), kind)
|
||||||
|
|
||||||
|
case reflect.Float64:
|
||||||
|
if rightValue.Kind() == reflect.Int {
|
||||||
|
return impl.compareNumber(leftValue.Float(), float64(rightValue.Int()), kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
return impl.compareNumber(leftValue.Float(), rightValue.Float(), kind)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("TODO: evaluateCompare not implemented %+v", reflect.TypeOf(leftValue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) coerceToNumber(value reflect.Value) reflect.Value {
|
||||||
|
switch value.Kind() {
|
||||||
|
case reflect.Invalid:
|
||||||
|
return reflect.ValueOf(0)
|
||||||
|
|
||||||
|
case reflect.Bool:
|
||||||
|
switch value.Bool() {
|
||||||
|
case true:
|
||||||
|
return reflect.ValueOf(1)
|
||||||
|
case false:
|
||||||
|
return reflect.ValueOf(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
case reflect.String:
|
||||||
|
if value.String() == "" {
|
||||||
|
return reflect.ValueOf(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to parse the string as a number
|
||||||
|
evaluated, err := impl.Evaluate(value.String(), false)
|
||||||
|
if err != nil {
|
||||||
|
return reflect.ValueOf(math.NaN())
|
||||||
|
}
|
||||||
|
|
||||||
|
if value := reflect.ValueOf(evaluated); impl.isNumber(value) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return reflect.ValueOf(math.NaN())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) coerceToString(value reflect.Value) reflect.Value {
|
||||||
|
switch value.Kind() {
|
||||||
|
case reflect.Invalid:
|
||||||
|
return reflect.ValueOf("")
|
||||||
|
|
||||||
|
case reflect.Bool:
|
||||||
|
switch value.Bool() {
|
||||||
|
case true:
|
||||||
|
return reflect.ValueOf("true")
|
||||||
|
case false:
|
||||||
|
return reflect.ValueOf("false")
|
||||||
|
}
|
||||||
|
|
||||||
|
case reflect.String:
|
||||||
|
return value
|
||||||
|
|
||||||
|
case reflect.Int:
|
||||||
|
return reflect.ValueOf(fmt.Sprint(value))
|
||||||
|
|
||||||
|
case reflect.Float64:
|
||||||
|
if math.IsInf(value.Float(), 1) {
|
||||||
|
return reflect.ValueOf("Infinity")
|
||||||
|
} else if math.IsInf(value.Float(), -1) {
|
||||||
|
return reflect.ValueOf("-Infinity")
|
||||||
|
}
|
||||||
|
return reflect.ValueOf(fmt.Sprint(value))
|
||||||
|
|
||||||
|
case reflect.Slice:
|
||||||
|
return reflect.ValueOf("Array")
|
||||||
|
|
||||||
|
case reflect.Map:
|
||||||
|
return reflect.ValueOf("Object")
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) compareString(left string, right string, kind actionlint.CompareOpNodeKind) (bool, error) {
|
||||||
|
switch kind {
|
||||||
|
case actionlint.CompareOpNodeKindLess:
|
||||||
|
return left < right, nil
|
||||||
|
case actionlint.CompareOpNodeKindLessEq:
|
||||||
|
return left <= right, nil
|
||||||
|
case actionlint.CompareOpNodeKindGreater:
|
||||||
|
return left > right, nil
|
||||||
|
case actionlint.CompareOpNodeKindGreaterEq:
|
||||||
|
return left >= right, nil
|
||||||
|
case actionlint.CompareOpNodeKindEq:
|
||||||
|
return left == right, nil
|
||||||
|
case actionlint.CompareOpNodeKindNotEq:
|
||||||
|
return left != right, nil
|
||||||
|
default:
|
||||||
|
return false, fmt.Errorf("TODO: not implemented to compare '%+v'", kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) compareNumber(left float64, right float64, kind actionlint.CompareOpNodeKind) (bool, error) {
|
||||||
|
switch kind {
|
||||||
|
case actionlint.CompareOpNodeKindLess:
|
||||||
|
return left < right, nil
|
||||||
|
case actionlint.CompareOpNodeKindLessEq:
|
||||||
|
return left <= right, nil
|
||||||
|
case actionlint.CompareOpNodeKindGreater:
|
||||||
|
return left > right, nil
|
||||||
|
case actionlint.CompareOpNodeKindGreaterEq:
|
||||||
|
return left >= right, nil
|
||||||
|
case actionlint.CompareOpNodeKindEq:
|
||||||
|
return left == right, nil
|
||||||
|
case actionlint.CompareOpNodeKindNotEq:
|
||||||
|
return left != right, nil
|
||||||
|
default:
|
||||||
|
return false, fmt.Errorf("TODO: not implemented to compare '%+v'", kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) isTruthy(value reflect.Value) bool {
|
||||||
|
switch value.Kind() {
|
||||||
|
case reflect.Bool:
|
||||||
|
return value.Bool()
|
||||||
|
|
||||||
|
case reflect.String:
|
||||||
|
return value.String() != ""
|
||||||
|
|
||||||
|
case reflect.Int:
|
||||||
|
return value.Int() != 0
|
||||||
|
|
||||||
|
case reflect.Float64:
|
||||||
|
if math.IsNaN(value.Float()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.Float() != 0
|
||||||
|
|
||||||
|
case reflect.Map:
|
||||||
|
return true
|
||||||
|
|
||||||
|
case reflect.Slice:
|
||||||
|
return true
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) isNumber(value reflect.Value) bool {
|
||||||
|
switch value.Kind() {
|
||||||
|
case reflect.Int, reflect.Float64:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) getSafeValue(value reflect.Value) interface{} {
|
||||||
|
switch value.Kind() {
|
||||||
|
case reflect.Invalid:
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case reflect.Float64:
|
||||||
|
if value.Float() == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) evaluateLogicalCompare(compareNode *actionlint.LogicalOpNode) (interface{}, error) {
|
||||||
|
left, err := impl.evaluateNode(compareNode.Left)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
leftValue := reflect.ValueOf(left)
|
||||||
|
|
||||||
|
right, err := impl.evaluateNode(compareNode.Right)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rightValue := reflect.ValueOf(right)
|
||||||
|
|
||||||
|
switch compareNode.Kind {
|
||||||
|
case actionlint.LogicalOpNodeKindAnd:
|
||||||
|
if impl.isTruthy(leftValue) {
|
||||||
|
return impl.getSafeValue(rightValue), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return impl.getSafeValue(leftValue), nil
|
||||||
|
|
||||||
|
case actionlint.LogicalOpNodeKindOr:
|
||||||
|
if impl.isTruthy(leftValue) {
|
||||||
|
return impl.getSafeValue(leftValue), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return impl.getSafeValue(rightValue), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("Unable to compare incompatibles types '%s' and '%s'", leftValue.Kind(), rightValue.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
// nolint:gocyclo
|
||||||
|
func (impl *interperterImpl) evaluateFuncCall(funcCallNode *actionlint.FuncCallNode) (interface{}, error) {
|
||||||
|
args := make([]reflect.Value, 0)
|
||||||
|
|
||||||
|
for _, arg := range funcCallNode.Args {
|
||||||
|
value, err := impl.evaluateNode(arg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, reflect.ValueOf(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.ToLower(funcCallNode.Callee) {
|
||||||
|
case "contains":
|
||||||
|
return impl.contains(args[0], args[1])
|
||||||
|
case "startswith":
|
||||||
|
return impl.startsWith(args[0], args[1])
|
||||||
|
case "endswith":
|
||||||
|
return impl.endsWith(args[0], args[1])
|
||||||
|
case "format":
|
||||||
|
return impl.format(args[0], args[1:]...)
|
||||||
|
case "join":
|
||||||
|
if len(args) == 1 {
|
||||||
|
return impl.join(args[0], reflect.ValueOf(","))
|
||||||
|
}
|
||||||
|
return impl.join(args[0], args[1])
|
||||||
|
case "tojson":
|
||||||
|
return impl.toJSON(args[0])
|
||||||
|
case "fromjson":
|
||||||
|
return impl.fromJSON(args[0])
|
||||||
|
case "hashfiles":
|
||||||
|
return impl.hashFiles(args...)
|
||||||
|
case "always":
|
||||||
|
return impl.always()
|
||||||
|
case "success":
|
||||||
|
if impl.config.Context == "job" {
|
||||||
|
return impl.jobSuccess()
|
||||||
|
}
|
||||||
|
if impl.config.Context == "step" {
|
||||||
|
return impl.stepSuccess()
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("Context '%s' must be one of 'job' or 'step'", impl.config.Context)
|
||||||
|
case "failure":
|
||||||
|
if impl.config.Context == "job" {
|
||||||
|
return impl.jobFailure()
|
||||||
|
}
|
||||||
|
if impl.config.Context == "step" {
|
||||||
|
return impl.stepFailure()
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("Context '%s' must be one of 'job' or 'step'", impl.config.Context)
|
||||||
|
case "cancelled":
|
||||||
|
return impl.cancelled()
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("TODO: '%s' not implemented", funcCallNode.Callee)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,581 @@
|
||||||
|
package exprparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nektos/act/pkg/model"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLiterals(t *testing.T) {
|
||||||
|
table := []struct {
|
||||||
|
input string
|
||||||
|
expected interface{}
|
||||||
|
name string
|
||||||
|
}{
|
||||||
|
{"true", true, "true"},
|
||||||
|
{"false", false, "false"},
|
||||||
|
{"null", nil, "null"},
|
||||||
|
{"123", 123, "integer"},
|
||||||
|
{"-9.7", -9.7, "float"},
|
||||||
|
{"0xff", 255, "hex"},
|
||||||
|
{"-2.99e-2", -2.99e-2, "exponential"},
|
||||||
|
{"'foo'", "foo", "string"},
|
||||||
|
{"'it''s foo'", "it's foo", "string"},
|
||||||
|
}
|
||||||
|
|
||||||
|
env := &EvaluationEnvironment{}
|
||||||
|
|
||||||
|
for _, tt := range table {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, false)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expected, output)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOperators(t *testing.T) {
|
||||||
|
table := []struct {
|
||||||
|
input string
|
||||||
|
expected interface{}
|
||||||
|
name string
|
||||||
|
error string
|
||||||
|
}{
|
||||||
|
{"(false || (false || true))", true, "logical-grouping", ""},
|
||||||
|
{"github.action", "push", "property-dereference", ""},
|
||||||
|
{"github['action']", "push", "property-index", ""},
|
||||||
|
{"github.action[0]", nil, "string-index", "Unable to index on non-slice value: string"},
|
||||||
|
{"fromJSON('[0,1]')[1]", 1.0, "array-index", ""},
|
||||||
|
{"(github.event.commits.*.author.username)[0]", "someone", "array-index-0", ""},
|
||||||
|
{"!true", false, "not", ""},
|
||||||
|
{"1 < 2", true, "less-than", ""},
|
||||||
|
{`'b' <= 'a'`, false, "less-than-or-equal", ""},
|
||||||
|
{"1 > 2", false, "greater-than", ""},
|
||||||
|
{`'b' >= 'a'`, true, "greater-than-or-equal", ""},
|
||||||
|
{`'a' == 'a'`, true, "equal", ""},
|
||||||
|
{`'a' != 'a'`, false, "not-equal", ""},
|
||||||
|
{`true && false`, false, "and", ""},
|
||||||
|
{`true || false`, true, "or", ""},
|
||||||
|
{`fromJSON('{}') && true`, true, "and-boolean-object", ""},
|
||||||
|
{`fromJSON('{}') || false`, make(map[string]interface{}), "or-boolean-object", ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
env := &EvaluationEnvironment{
|
||||||
|
Github: &model.GithubContext{
|
||||||
|
Action: "push",
|
||||||
|
Event: map[string]interface{}{
|
||||||
|
"commits": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"author": map[string]interface{}{
|
||||||
|
"username": "someone",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"author": map[string]interface{}{
|
||||||
|
"username": "someone-else",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range table {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, false)
|
||||||
|
if tt.error != "" {
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
assert.Equal(t, tt.error, err.Error())
|
||||||
|
} else {
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expected, output)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOperatorsCompare(t *testing.T) {
|
||||||
|
table := []struct {
|
||||||
|
input string
|
||||||
|
expected interface{}
|
||||||
|
name string
|
||||||
|
}{
|
||||||
|
{"!null", true, "not-null"},
|
||||||
|
{"!-10", false, "not-neg-num"},
|
||||||
|
{"!0", true, "not-zero"},
|
||||||
|
{"!3.14", false, "not-pos-float"},
|
||||||
|
{"!''", true, "not-empty-str"},
|
||||||
|
{"!'abc'", false, "not-str"},
|
||||||
|
{"!fromJSON('{}')", false, "not-obj"},
|
||||||
|
{"!fromJSON('[]')", false, "not-arr"},
|
||||||
|
{`null == 0 }}`, true, "null-coercion"},
|
||||||
|
{`true == 1 }}`, true, "boolean-coercion"},
|
||||||
|
{`'' == 0 }}`, true, "string-0-coercion"},
|
||||||
|
{`'3' == 3 }}`, true, "string-3-coercion"},
|
||||||
|
{`0 == null }}`, true, "null-coercion-alt"},
|
||||||
|
{`1 == true }}`, true, "boolean-coercion-alt"},
|
||||||
|
{`0 == '' }}`, true, "string-0-coercion-alt"},
|
||||||
|
{`3 == '3' }}`, true, "string-3-coercion-alt"},
|
||||||
|
{`'TEST' == 'test' }}`, true, "string-casing"},
|
||||||
|
{`fromJSON('{}') < 2 }}`, false, "object-with-less"},
|
||||||
|
{`fromJSON('{}') < fromJSON('[]') }}`, false, "object/arr-with-lt"},
|
||||||
|
{`fromJSON('{}') > fromJSON('[]') }}`, false, "object/arr-with-gt"},
|
||||||
|
}
|
||||||
|
|
||||||
|
env := &EvaluationEnvironment{
|
||||||
|
Github: &model.GithubContext{
|
||||||
|
Action: "push",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range table {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, false)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expected, output)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOperatorsBooleanEvaluation(t *testing.T) {
|
||||||
|
table := []struct {
|
||||||
|
input string
|
||||||
|
expected interface{}
|
||||||
|
name string
|
||||||
|
}{
|
||||||
|
// true &&
|
||||||
|
{"true && true", true, "true-and"},
|
||||||
|
{"true && false", false, "true-and"},
|
||||||
|
{"true && null", nil, "true-and"},
|
||||||
|
{"true && -10", -10, "true-and"},
|
||||||
|
{"true && 0", 0, "true-and"},
|
||||||
|
{"true && 10", 10, "true-and"},
|
||||||
|
{"true && 3.14", 3.14, "true-and"},
|
||||||
|
{"true && 0.0", 0, "true-and"},
|
||||||
|
{"true && Infinity", math.Inf(1), "true-and"},
|
||||||
|
// {"true && -Infinity", math.Inf(-1), "true-and"},
|
||||||
|
{"true && NaN", math.NaN(), "true-and"},
|
||||||
|
{"true && ''", "", "true-and"},
|
||||||
|
{"true && 'abc'", "abc", "true-and"},
|
||||||
|
// false &&
|
||||||
|
{"false && true", false, "false-and"},
|
||||||
|
{"false && false", false, "false-and"},
|
||||||
|
{"false && null", false, "false-and"},
|
||||||
|
{"false && -10", false, "false-and"},
|
||||||
|
{"false && 0", false, "false-and"},
|
||||||
|
{"false && 10", false, "false-and"},
|
||||||
|
{"false && 3.14", false, "false-and"},
|
||||||
|
{"false && 0.0", false, "false-and"},
|
||||||
|
{"false && Infinity", false, "false-and"},
|
||||||
|
// {"false && -Infinity", false, "false-and"},
|
||||||
|
{"false && NaN", false, "false-and"},
|
||||||
|
{"false && ''", false, "false-and"},
|
||||||
|
{"false && 'abc'", false, "false-and"},
|
||||||
|
// true ||
|
||||||
|
{"true || true", true, "true-or"},
|
||||||
|
{"true || false", true, "true-or"},
|
||||||
|
{"true || null", true, "true-or"},
|
||||||
|
{"true || -10", true, "true-or"},
|
||||||
|
{"true || 0", true, "true-or"},
|
||||||
|
{"true || 10", true, "true-or"},
|
||||||
|
{"true || 3.14", true, "true-or"},
|
||||||
|
{"true || 0.0", true, "true-or"},
|
||||||
|
{"true || Infinity", true, "true-or"},
|
||||||
|
// {"true || -Infinity", true, "true-or"},
|
||||||
|
{"true || NaN", true, "true-or"},
|
||||||
|
{"true || ''", true, "true-or"},
|
||||||
|
{"true || 'abc'", true, "true-or"},
|
||||||
|
// false ||
|
||||||
|
{"false || true", true, "false-or"},
|
||||||
|
{"false || false", false, "false-or"},
|
||||||
|
{"false || null", nil, "false-or"},
|
||||||
|
{"false || -10", -10, "false-or"},
|
||||||
|
{"false || 0", 0, "false-or"},
|
||||||
|
{"false || 10", 10, "false-or"},
|
||||||
|
{"false || 3.14", 3.14, "false-or"},
|
||||||
|
{"false || 0.0", 0, "false-or"},
|
||||||
|
{"false || Infinity", math.Inf(1), "false-or"},
|
||||||
|
// {"false || -Infinity", math.Inf(-1), "false-or"},
|
||||||
|
{"false || NaN", math.NaN(), "false-or"},
|
||||||
|
{"false || ''", "", "false-or"},
|
||||||
|
{"false || 'abc'", "abc", "false-or"},
|
||||||
|
// null &&
|
||||||
|
{"null && true", nil, "null-and"},
|
||||||
|
{"null && false", nil, "null-and"},
|
||||||
|
{"null && null", nil, "null-and"},
|
||||||
|
{"null && -10", nil, "null-and"},
|
||||||
|
{"null && 0", nil, "null-and"},
|
||||||
|
{"null && 10", nil, "null-and"},
|
||||||
|
{"null && 3.14", nil, "null-and"},
|
||||||
|
{"null && 0.0", nil, "null-and"},
|
||||||
|
{"null && Infinity", nil, "null-and"},
|
||||||
|
// {"null && -Infinity", nil, "null-and"},
|
||||||
|
{"null && NaN", nil, "null-and"},
|
||||||
|
{"null && ''", nil, "null-and"},
|
||||||
|
{"null && 'abc'", nil, "null-and"},
|
||||||
|
// null ||
|
||||||
|
{"null || true", true, "null-or"},
|
||||||
|
{"null || false", false, "null-or"},
|
||||||
|
{"null || null", nil, "null-or"},
|
||||||
|
{"null || -10", -10, "null-or"},
|
||||||
|
{"null || 0", 0, "null-or"},
|
||||||
|
{"null || 10", 10, "null-or"},
|
||||||
|
{"null || 3.14", 3.14, "null-or"},
|
||||||
|
{"null || 0.0", 0, "null-or"},
|
||||||
|
{"null || Infinity", math.Inf(1), "null-or"},
|
||||||
|
// {"null || -Infinity", math.Inf(-1), "null-or"},
|
||||||
|
{"null || NaN", math.NaN(), "null-or"},
|
||||||
|
{"null || ''", "", "null-or"},
|
||||||
|
{"null || 'abc'", "abc", "null-or"},
|
||||||
|
// -10 &&
|
||||||
|
{"-10 && true", true, "neg-num-and"},
|
||||||
|
{"-10 && false", false, "neg-num-and"},
|
||||||
|
{"-10 && null", nil, "neg-num-and"},
|
||||||
|
{"-10 && -10", -10, "neg-num-and"},
|
||||||
|
{"-10 && 0", 0, "neg-num-and"},
|
||||||
|
{"-10 && 10", 10, "neg-num-and"},
|
||||||
|
{"-10 && 3.14", 3.14, "neg-num-and"},
|
||||||
|
{"-10 && 0.0", 0, "neg-num-and"},
|
||||||
|
{"-10 && Infinity", math.Inf(1), "neg-num-and"},
|
||||||
|
// {"-10 && -Infinity", math.Inf(-1), "neg-num-and"},
|
||||||
|
{"-10 && NaN", math.NaN(), "neg-num-and"},
|
||||||
|
{"-10 && ''", "", "neg-num-and"},
|
||||||
|
{"-10 && 'abc'", "abc", "neg-num-and"},
|
||||||
|
// -10 ||
|
||||||
|
{"-10 || true", -10, "neg-num-or"},
|
||||||
|
{"-10 || false", -10, "neg-num-or"},
|
||||||
|
{"-10 || null", -10, "neg-num-or"},
|
||||||
|
{"-10 || -10", -10, "neg-num-or"},
|
||||||
|
{"-10 || 0", -10, "neg-num-or"},
|
||||||
|
{"-10 || 10", -10, "neg-num-or"},
|
||||||
|
{"-10 || 3.14", -10, "neg-num-or"},
|
||||||
|
{"-10 || 0.0", -10, "neg-num-or"},
|
||||||
|
{"-10 || Infinity", -10, "neg-num-or"},
|
||||||
|
// {"-10 || -Infinity", -10, "neg-num-or"},
|
||||||
|
{"-10 || NaN", -10, "neg-num-or"},
|
||||||
|
{"-10 || ''", -10, "neg-num-or"},
|
||||||
|
{"-10 || 'abc'", -10, "neg-num-or"},
|
||||||
|
// 0 &&
|
||||||
|
{"0 && true", 0, "zero-and"},
|
||||||
|
{"0 && false", 0, "zero-and"},
|
||||||
|
{"0 && null", 0, "zero-and"},
|
||||||
|
{"0 && -10", 0, "zero-and"},
|
||||||
|
{"0 && 0", 0, "zero-and"},
|
||||||
|
{"0 && 10", 0, "zero-and"},
|
||||||
|
{"0 && 3.14", 0, "zero-and"},
|
||||||
|
{"0 && 0.0", 0, "zero-and"},
|
||||||
|
{"0 && Infinity", 0, "zero-and"},
|
||||||
|
// {"0 && -Infinity", 0, "zero-and"},
|
||||||
|
{"0 && NaN", 0, "zero-and"},
|
||||||
|
{"0 && ''", 0, "zero-and"},
|
||||||
|
{"0 && 'abc'", 0, "zero-and"},
|
||||||
|
// 0 ||
|
||||||
|
{"0 || true", true, "zero-or"},
|
||||||
|
{"0 || false", false, "zero-or"},
|
||||||
|
{"0 || null", nil, "zero-or"},
|
||||||
|
{"0 || -10", -10, "zero-or"},
|
||||||
|
{"0 || 0", 0, "zero-or"},
|
||||||
|
{"0 || 10", 10, "zero-or"},
|
||||||
|
{"0 || 3.14", 3.14, "zero-or"},
|
||||||
|
{"0 || 0.0", 0, "zero-or"},
|
||||||
|
{"0 || Infinity", math.Inf(1), "zero-or"},
|
||||||
|
// {"0 || -Infinity", math.Inf(-1), "zero-or"},
|
||||||
|
{"0 || NaN", math.NaN(), "zero-or"},
|
||||||
|
{"0 || ''", "", "zero-or"},
|
||||||
|
{"0 || 'abc'", "abc", "zero-or"},
|
||||||
|
// 10 &&
|
||||||
|
{"10 && true", true, "pos-num-and"},
|
||||||
|
{"10 && false", false, "pos-num-and"},
|
||||||
|
{"10 && null", nil, "pos-num-and"},
|
||||||
|
{"10 && -10", -10, "pos-num-and"},
|
||||||
|
{"10 && 0", 0, "pos-num-and"},
|
||||||
|
{"10 && 10", 10, "pos-num-and"},
|
||||||
|
{"10 && 3.14", 3.14, "pos-num-and"},
|
||||||
|
{"10 && 0.0", 0, "pos-num-and"},
|
||||||
|
{"10 && Infinity", math.Inf(1), "pos-num-and"},
|
||||||
|
// {"10 && -Infinity", math.Inf(-1), "pos-num-and"},
|
||||||
|
{"10 && NaN", math.NaN(), "pos-num-and"},
|
||||||
|
{"10 && ''", "", "pos-num-and"},
|
||||||
|
{"10 && 'abc'", "abc", "pos-num-and"},
|
||||||
|
// 10 ||
|
||||||
|
{"10 || true", 10, "pos-num-or"},
|
||||||
|
{"10 || false", 10, "pos-num-or"},
|
||||||
|
{"10 || null", 10, "pos-num-or"},
|
||||||
|
{"10 || -10", 10, "pos-num-or"},
|
||||||
|
{"10 || 0", 10, "pos-num-or"},
|
||||||
|
{"10 || 10", 10, "pos-num-or"},
|
||||||
|
{"10 || 3.14", 10, "pos-num-or"},
|
||||||
|
{"10 || 0.0", 10, "pos-num-or"},
|
||||||
|
{"10 || Infinity", 10, "pos-num-or"},
|
||||||
|
// {"10 || -Infinity", 10, "pos-num-or"},
|
||||||
|
{"10 || NaN", 10, "pos-num-or"},
|
||||||
|
{"10 || ''", 10, "pos-num-or"},
|
||||||
|
{"10 || 'abc'", 10, "pos-num-or"},
|
||||||
|
// 3.14 &&
|
||||||
|
{"3.14 && true", true, "pos-float-and"},
|
||||||
|
{"3.14 && false", false, "pos-float-and"},
|
||||||
|
{"3.14 && null", nil, "pos-float-and"},
|
||||||
|
{"3.14 && -10", -10, "pos-float-and"},
|
||||||
|
{"3.14 && 0", 0, "pos-float-and"},
|
||||||
|
{"3.14 && 10", 10, "pos-float-and"},
|
||||||
|
{"3.14 && 3.14", 3.14, "pos-float-and"},
|
||||||
|
{"3.14 && 0.0", 0, "pos-float-and"},
|
||||||
|
{"3.14 && Infinity", math.Inf(1), "pos-float-and"},
|
||||||
|
// {"3.14 && -Infinity", math.Inf(-1), "pos-float-and"},
|
||||||
|
{"3.14 && NaN", math.NaN(), "pos-float-and"},
|
||||||
|
{"3.14 && ''", "", "pos-float-and"},
|
||||||
|
{"3.14 && 'abc'", "abc", "pos-float-and"},
|
||||||
|
// 3.14 ||
|
||||||
|
{"3.14 || true", 3.14, "pos-float-or"},
|
||||||
|
{"3.14 || false", 3.14, "pos-float-or"},
|
||||||
|
{"3.14 || null", 3.14, "pos-float-or"},
|
||||||
|
{"3.14 || -10", 3.14, "pos-float-or"},
|
||||||
|
{"3.14 || 0", 3.14, "pos-float-or"},
|
||||||
|
{"3.14 || 10", 3.14, "pos-float-or"},
|
||||||
|
{"3.14 || 3.14", 3.14, "pos-float-or"},
|
||||||
|
{"3.14 || 0.0", 3.14, "pos-float-or"},
|
||||||
|
{"3.14 || Infinity", 3.14, "pos-float-or"},
|
||||||
|
// {"3.14 || -Infinity", 3.14, "pos-float-or"},
|
||||||
|
{"3.14 || NaN", 3.14, "pos-float-or"},
|
||||||
|
{"3.14 || ''", 3.14, "pos-float-or"},
|
||||||
|
{"3.14 || 'abc'", 3.14, "pos-float-or"},
|
||||||
|
// Infinity &&
|
||||||
|
{"Infinity && true", true, "pos-inf-and"},
|
||||||
|
{"Infinity && false", false, "pos-inf-and"},
|
||||||
|
{"Infinity && null", nil, "pos-inf-and"},
|
||||||
|
{"Infinity && -10", -10, "pos-inf-and"},
|
||||||
|
{"Infinity && 0", 0, "pos-inf-and"},
|
||||||
|
{"Infinity && 10", 10, "pos-inf-and"},
|
||||||
|
{"Infinity && 3.14", 3.14, "pos-inf-and"},
|
||||||
|
{"Infinity && 0.0", 0, "pos-inf-and"},
|
||||||
|
{"Infinity && Infinity", math.Inf(1), "pos-inf-and"},
|
||||||
|
// {"Infinity && -Infinity", math.Inf(-1), "pos-inf-and"},
|
||||||
|
{"Infinity && NaN", math.NaN(), "pos-inf-and"},
|
||||||
|
{"Infinity && ''", "", "pos-inf-and"},
|
||||||
|
{"Infinity && 'abc'", "abc", "pos-inf-and"},
|
||||||
|
// Infinity ||
|
||||||
|
{"Infinity || true", math.Inf(1), "pos-inf-or"},
|
||||||
|
{"Infinity || false", math.Inf(1), "pos-inf-or"},
|
||||||
|
{"Infinity || null", math.Inf(1), "pos-inf-or"},
|
||||||
|
{"Infinity || -10", math.Inf(1), "pos-inf-or"},
|
||||||
|
{"Infinity || 0", math.Inf(1), "pos-inf-or"},
|
||||||
|
{"Infinity || 10", math.Inf(1), "pos-inf-or"},
|
||||||
|
{"Infinity || 3.14", math.Inf(1), "pos-inf-or"},
|
||||||
|
{"Infinity || 0.0", math.Inf(1), "pos-inf-or"},
|
||||||
|
{"Infinity || Infinity", math.Inf(1), "pos-inf-or"},
|
||||||
|
// {"Infinity || -Infinity", math.Inf(1), "pos-inf-or"},
|
||||||
|
{"Infinity || NaN", math.Inf(1), "pos-inf-or"},
|
||||||
|
{"Infinity || ''", math.Inf(1), "pos-inf-or"},
|
||||||
|
{"Infinity || 'abc'", math.Inf(1), "pos-inf-or"},
|
||||||
|
// -Infinity &&
|
||||||
|
// {"-Infinity && true", true, "neg-inf-and"},
|
||||||
|
// {"-Infinity && false", false, "neg-inf-and"},
|
||||||
|
// {"-Infinity && null", nil, "neg-inf-and"},
|
||||||
|
// {"-Infinity && -10", -10, "neg-inf-and"},
|
||||||
|
// {"-Infinity && 0", 0, "neg-inf-and"},
|
||||||
|
// {"-Infinity && 10", 10, "neg-inf-and"},
|
||||||
|
// {"-Infinity && 3.14", 3.14, "neg-inf-and"},
|
||||||
|
// {"-Infinity && 0.0", 0, "neg-inf-and"},
|
||||||
|
// {"-Infinity && Infinity", math.Inf(1), "neg-inf-and"},
|
||||||
|
// {"-Infinity && -Infinity", math.Inf(-1), "neg-inf-and"},
|
||||||
|
// {"-Infinity && NaN", math.NaN(), "neg-inf-and"},
|
||||||
|
// {"-Infinity && ''", "", "neg-inf-and"},
|
||||||
|
// {"-Infinity && 'abc'", "abc", "neg-inf-and"},
|
||||||
|
// -Infinity ||
|
||||||
|
// {"-Infinity || true", math.Inf(-1), "neg-inf-or"},
|
||||||
|
// {"-Infinity || false", math.Inf(-1), "neg-inf-or"},
|
||||||
|
// {"-Infinity || null", math.Inf(-1), "neg-inf-or"},
|
||||||
|
// {"-Infinity || -10", math.Inf(-1), "neg-inf-or"},
|
||||||
|
// {"-Infinity || 0", math.Inf(-1), "neg-inf-or"},
|
||||||
|
// {"-Infinity || 10", math.Inf(-1), "neg-inf-or"},
|
||||||
|
// {"-Infinity || 3.14", math.Inf(-1), "neg-inf-or"},
|
||||||
|
// {"-Infinity || 0.0", math.Inf(-1), "neg-inf-or"},
|
||||||
|
// {"-Infinity || Infinity", math.Inf(-1), "neg-inf-or"},
|
||||||
|
// {"-Infinity || -Infinity", math.Inf(-1), "neg-inf-or"},
|
||||||
|
// {"-Infinity || NaN", math.Inf(-1), "neg-inf-or"},
|
||||||
|
// {"-Infinity || ''", math.Inf(-1), "neg-inf-or"},
|
||||||
|
// {"-Infinity || 'abc'", math.Inf(-1), "neg-inf-or"},
|
||||||
|
// NaN &&
|
||||||
|
{"NaN && true", math.NaN(), "nan-and"},
|
||||||
|
{"NaN && false", math.NaN(), "nan-and"},
|
||||||
|
{"NaN && null", math.NaN(), "nan-and"},
|
||||||
|
{"NaN && -10", math.NaN(), "nan-and"},
|
||||||
|
{"NaN && 0", math.NaN(), "nan-and"},
|
||||||
|
{"NaN && 10", math.NaN(), "nan-and"},
|
||||||
|
{"NaN && 3.14", math.NaN(), "nan-and"},
|
||||||
|
{"NaN && 0.0", math.NaN(), "nan-and"},
|
||||||
|
{"NaN && Infinity", math.NaN(), "nan-and"},
|
||||||
|
// {"NaN && -Infinity", math.NaN(), "nan-and"},
|
||||||
|
{"NaN && NaN", math.NaN(), "nan-and"},
|
||||||
|
{"NaN && ''", math.NaN(), "nan-and"},
|
||||||
|
{"NaN && 'abc'", math.NaN(), "nan-and"},
|
||||||
|
// NaN ||
|
||||||
|
{"NaN || true", true, "nan-or"},
|
||||||
|
{"NaN || false", false, "nan-or"},
|
||||||
|
{"NaN || null", nil, "nan-or"},
|
||||||
|
{"NaN || -10", -10, "nan-or"},
|
||||||
|
{"NaN || 0", 0, "nan-or"},
|
||||||
|
{"NaN || 10", 10, "nan-or"},
|
||||||
|
{"NaN || 3.14", 3.14, "nan-or"},
|
||||||
|
{"NaN || 0.0", 0, "nan-or"},
|
||||||
|
{"NaN || Infinity", math.Inf(1), "nan-or"},
|
||||||
|
// {"NaN || -Infinity", math.Inf(-1), "nan-or"},
|
||||||
|
{"NaN || NaN", math.NaN(), "nan-or"},
|
||||||
|
{"NaN || ''", "", "nan-or"},
|
||||||
|
{"NaN || 'abc'", "abc", "nan-or"},
|
||||||
|
// "" &&
|
||||||
|
{"'' && true", "", "empty-str-and"},
|
||||||
|
{"'' && false", "", "empty-str-and"},
|
||||||
|
{"'' && null", "", "empty-str-and"},
|
||||||
|
{"'' && -10", "", "empty-str-and"},
|
||||||
|
{"'' && 0", "", "empty-str-and"},
|
||||||
|
{"'' && 10", "", "empty-str-and"},
|
||||||
|
{"'' && 3.14", "", "empty-str-and"},
|
||||||
|
{"'' && 0.0", "", "empty-str-and"},
|
||||||
|
{"'' && Infinity", "", "empty-str-and"},
|
||||||
|
// {"'' && -Infinity", "", "empty-str-and"},
|
||||||
|
{"'' && NaN", "", "empty-str-and"},
|
||||||
|
{"'' && ''", "", "empty-str-and"},
|
||||||
|
{"'' && 'abc'", "", "empty-str-and"},
|
||||||
|
// "" ||
|
||||||
|
{"'' || true", true, "empty-str-or"},
|
||||||
|
{"'' || false", false, "empty-str-or"},
|
||||||
|
{"'' || null", nil, "empty-str-or"},
|
||||||
|
{"'' || -10", -10, "empty-str-or"},
|
||||||
|
{"'' || 0", 0, "empty-str-or"},
|
||||||
|
{"'' || 10", 10, "empty-str-or"},
|
||||||
|
{"'' || 3.14", 3.14, "empty-str-or"},
|
||||||
|
{"'' || 0.0", 0, "empty-str-or"},
|
||||||
|
{"'' || Infinity", math.Inf(1), "empty-str-or"},
|
||||||
|
// {"'' || -Infinity", math.Inf(-1), "empty-str-or"},
|
||||||
|
{"'' || NaN", math.NaN(), "empty-str-or"},
|
||||||
|
{"'' || ''", "", "empty-str-or"},
|
||||||
|
{"'' || 'abc'", "abc", "empty-str-or"},
|
||||||
|
// "abc" &&
|
||||||
|
{"'abc' && true", true, "str-and"},
|
||||||
|
{"'abc' && false", false, "str-and"},
|
||||||
|
{"'abc' && null", nil, "str-and"},
|
||||||
|
{"'abc' && -10", -10, "str-and"},
|
||||||
|
{"'abc' && 0", 0, "str-and"},
|
||||||
|
{"'abc' && 10", 10, "str-and"},
|
||||||
|
{"'abc' && 3.14", 3.14, "str-and"},
|
||||||
|
{"'abc' && 0.0", 0, "str-and"},
|
||||||
|
{"'abc' && Infinity", math.Inf(1), "str-and"},
|
||||||
|
// {"'abc' && -Infinity", math.Inf(-1), "str-and"},
|
||||||
|
{"'abc' && NaN", math.NaN(), "str-and"},
|
||||||
|
{"'abc' && ''", "", "str-and"},
|
||||||
|
{"'abc' && 'abc'", "abc", "str-and"},
|
||||||
|
// "abc" ||
|
||||||
|
{"'abc' || true", "abc", "str-or"},
|
||||||
|
{"'abc' || false", "abc", "str-or"},
|
||||||
|
{"'abc' || null", "abc", "str-or"},
|
||||||
|
{"'abc' || -10", "abc", "str-or"},
|
||||||
|
{"'abc' || 0", "abc", "str-or"},
|
||||||
|
{"'abc' || 10", "abc", "str-or"},
|
||||||
|
{"'abc' || 3.14", "abc", "str-or"},
|
||||||
|
{"'abc' || 0.0", "abc", "str-or"},
|
||||||
|
{"'abc' || Infinity", "abc", "str-or"},
|
||||||
|
// {"'abc' || -Infinity", "abc", "str-or"},
|
||||||
|
{"'abc' || NaN", "abc", "str-or"},
|
||||||
|
{"'abc' || ''", "abc", "str-or"},
|
||||||
|
{"'abc' || 'abc'", "abc", "str-or"},
|
||||||
|
// extra tests
|
||||||
|
{"0.0 && true", 0, "float-evaluation-0-alt"},
|
||||||
|
{"-1.5 && true", true, "float-evaluation-neg-alt"},
|
||||||
|
}
|
||||||
|
|
||||||
|
env := &EvaluationEnvironment{
|
||||||
|
Github: &model.GithubContext{
|
||||||
|
Action: "push",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range table {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, false)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
if expected, ok := tt.expected.(float64); ok && math.IsNaN(expected) {
|
||||||
|
assert.True(t, math.IsNaN(output.(float64)))
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, tt.expected, output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContexts(t *testing.T) {
|
||||||
|
table := []struct {
|
||||||
|
input string
|
||||||
|
expected interface{}
|
||||||
|
name string
|
||||||
|
}{
|
||||||
|
{"github.action", "push", "github-context"},
|
||||||
|
{"env.TEST", "value", "env-context"},
|
||||||
|
{"job.status", "success", "job-context"},
|
||||||
|
{"steps.step-id.outputs.name", "value", "steps-context"},
|
||||||
|
{"runner.os", "Linux", "runner-context"},
|
||||||
|
{"secrets.name", "value", "secrets-context"},
|
||||||
|
{"strategy.fail-fast", true, "strategy-context"},
|
||||||
|
{"matrix.os", "Linux", "matrix-context"},
|
||||||
|
{"needs.job-id.outputs.output-name", "value", "needs-context"},
|
||||||
|
{"inputs.name", "value", "inputs-context"},
|
||||||
|
}
|
||||||
|
|
||||||
|
env := &EvaluationEnvironment{
|
||||||
|
Github: &model.GithubContext{
|
||||||
|
Action: "push",
|
||||||
|
},
|
||||||
|
Env: map[string]string{
|
||||||
|
"TEST": "value",
|
||||||
|
},
|
||||||
|
Job: &model.JobContext{
|
||||||
|
Status: "success",
|
||||||
|
},
|
||||||
|
Steps: map[string]*model.StepResult{
|
||||||
|
"step-id": {
|
||||||
|
Outputs: map[string]string{
|
||||||
|
"name": "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Runner: map[string]interface{}{
|
||||||
|
"os": "Linux",
|
||||||
|
"temp": "/tmp",
|
||||||
|
"tool_cache": "/opt/hostedtoolcache",
|
||||||
|
},
|
||||||
|
Secrets: map[string]string{
|
||||||
|
"name": "value",
|
||||||
|
},
|
||||||
|
Strategy: map[string]interface{}{
|
||||||
|
"fail-fast": true,
|
||||||
|
},
|
||||||
|
Matrix: map[string]interface{}{
|
||||||
|
"os": "Linux",
|
||||||
|
},
|
||||||
|
Needs: map[string]map[string]map[string]string{
|
||||||
|
"job-id": {
|
||||||
|
"outputs": {
|
||||||
|
"output-name": "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Inputs: map[string]interface{}{
|
||||||
|
"name": "value",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range table {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, false)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expected, output)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
Hello
|
|
@ -0,0 +1 @@
|
||||||
|
World!
|
|
@ -1,460 +1,33 @@
|
||||||
package runner
|
package runner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"math"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/robertkrimen/otto"
|
"github.com/nektos/act/pkg/exprparser"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
var expressionPattern, operatorPattern *regexp.Regexp
|
// ExpressionEvaluator is the interface for evaluating expressions
|
||||||
|
type ExpressionEvaluator interface {
|
||||||
func init() {
|
evaluate(string, bool) (interface{}, error)
|
||||||
expressionPattern = regexp.MustCompile(`\${{\s*(.+?)\s*}}`)
|
Interpolate(string) string
|
||||||
operatorPattern = regexp.MustCompile("^[!=><|&]+$")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewExpressionEvaluator creates a new evaluator
|
// NewExpressionEvaluator creates a new evaluator
|
||||||
func (rc *RunContext) NewExpressionEvaluator() ExpressionEvaluator {
|
func (rc *RunContext) NewExpressionEvaluator() ExpressionEvaluator {
|
||||||
vm := rc.newVM()
|
// todo: cleanup EvaluationEnvironment creation
|
||||||
|
job := rc.Run.Job()
|
||||||
return &expressionEvaluator{
|
strategy := make(map[string]interface{})
|
||||||
vm,
|
if job.Strategy != nil {
|
||||||
}
|
strategy["fail-fast"] = job.Strategy.FailFast
|
||||||
}
|
strategy["max-parallel"] = job.Strategy.MaxParallel
|
||||||
|
|
||||||
// NewExpressionEvaluator creates a new evaluator
|
|
||||||
func (sc *StepContext) NewExpressionEvaluator() ExpressionEvaluator {
|
|
||||||
vm := sc.RunContext.newVM()
|
|
||||||
configers := []func(*otto.Otto){
|
|
||||||
sc.vmEnv(),
|
|
||||||
sc.vmNeeds(),
|
|
||||||
sc.vmSuccess(),
|
|
||||||
sc.vmFailure(),
|
|
||||||
}
|
|
||||||
for _, configer := range configers {
|
|
||||||
configer(vm)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &expressionEvaluator{
|
jobs := rc.Run.Workflow.Jobs
|
||||||
vm,
|
jobNeeds := rc.Run.Job().Needs()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExpressionEvaluator is the interface for evaluating expressions
|
|
||||||
type ExpressionEvaluator interface {
|
|
||||||
Evaluate(string) (string, bool, error)
|
|
||||||
Interpolate(string) string
|
|
||||||
InterpolateWithStringCheck(string) (string, bool)
|
|
||||||
Rewrite(string) string
|
|
||||||
}
|
|
||||||
|
|
||||||
type expressionEvaluator struct {
|
|
||||||
vm *otto.Otto
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ee *expressionEvaluator) Evaluate(in string) (string, bool, error) {
|
|
||||||
if strings.HasPrefix(in, `secrets.`) {
|
|
||||||
in = `secrets.` + strings.ToUpper(strings.SplitN(in, `.`, 2)[1])
|
|
||||||
}
|
|
||||||
re := ee.Rewrite(in)
|
|
||||||
if re != in {
|
|
||||||
log.Debugf("Evaluating '%s' instead of '%s'", re, in)
|
|
||||||
}
|
|
||||||
|
|
||||||
val, err := ee.vm.Run(re)
|
|
||||||
if err != nil {
|
|
||||||
return "", false, err
|
|
||||||
}
|
|
||||||
if val.IsNull() || val.IsUndefined() {
|
|
||||||
return "", false, nil
|
|
||||||
}
|
|
||||||
valAsString, err := val.ToString()
|
|
||||||
if err != nil {
|
|
||||||
return "", false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return valAsString, val.IsString(), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ee *expressionEvaluator) Interpolate(in string) string {
|
|
||||||
interpolated, _ := ee.InterpolateWithStringCheck(in)
|
|
||||||
return interpolated
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ee *expressionEvaluator) InterpolateWithStringCheck(in string) (string, bool) {
|
|
||||||
errList := make([]error, 0)
|
|
||||||
|
|
||||||
out := in
|
|
||||||
isString := false
|
|
||||||
for {
|
|
||||||
out = expressionPattern.ReplaceAllStringFunc(in, func(match string) string {
|
|
||||||
// Extract and trim the actual expression inside ${{...}} delimiters
|
|
||||||
expression := expressionPattern.ReplaceAllString(match, "$1")
|
|
||||||
|
|
||||||
// Evaluate the expression and retrieve errors if any
|
|
||||||
evaluated, evaluatedIsString, err := ee.Evaluate(expression)
|
|
||||||
if err != nil {
|
|
||||||
errList = append(errList, err)
|
|
||||||
}
|
|
||||||
isString = evaluatedIsString
|
|
||||||
return evaluated
|
|
||||||
})
|
|
||||||
if len(errList) > 0 {
|
|
||||||
log.Errorf("Unable to interpolate string '%s' - %v", in, errList)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if out == in {
|
|
||||||
// No replacement occurred, we're done!
|
|
||||||
break
|
|
||||||
}
|
|
||||||
in = out
|
|
||||||
}
|
|
||||||
return out, isString
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rewrite tries to transform any javascript property accessor into its bracket notation.
|
|
||||||
// For instance, "object.property" would become "object['property']".
|
|
||||||
func (ee *expressionEvaluator) Rewrite(in string) string {
|
|
||||||
var buf strings.Builder
|
|
||||||
r := strings.NewReader(in)
|
|
||||||
for {
|
|
||||||
c, _, err := r.ReadRune()
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
//nolint
|
|
||||||
switch {
|
|
||||||
default:
|
|
||||||
buf.WriteRune(c)
|
|
||||||
case c == '\'':
|
|
||||||
buf.WriteRune(c)
|
|
||||||
ee.advString(&buf, r)
|
|
||||||
case c == '.':
|
|
||||||
buf.WriteString("['")
|
|
||||||
ee.advPropertyName(&buf, r)
|
|
||||||
buf.WriteString("']")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*expressionEvaluator) advString(w *strings.Builder, r *strings.Reader) error {
|
|
||||||
for {
|
|
||||||
c, _, err := r.ReadRune()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if c != '\'' {
|
|
||||||
w.WriteRune(c)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handles a escaped string: ex. 'It''s ok'
|
|
||||||
c, _, err = r.ReadRune()
|
|
||||||
if err != nil {
|
|
||||||
w.WriteString("'")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if c != '\'' {
|
|
||||||
w.WriteString("'")
|
|
||||||
if err := r.UnreadRune(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
w.WriteString(`\'`)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*expressionEvaluator) advPropertyName(w *strings.Builder, r *strings.Reader) error {
|
|
||||||
for {
|
|
||||||
c, _, err := r.ReadRune()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !isLetter(c) {
|
|
||||||
if err := r.UnreadRune(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
w.WriteRune(c)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isLetter(c rune) bool {
|
|
||||||
switch {
|
|
||||||
case c >= 'a' && c <= 'z':
|
|
||||||
return true
|
|
||||||
case c >= 'A' && c <= 'Z':
|
|
||||||
return true
|
|
||||||
case c >= '0' && c <= '9':
|
|
||||||
return true
|
|
||||||
case c == '_' || c == '-':
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *RunContext) newVM() *otto.Otto {
|
|
||||||
configers := []func(*otto.Otto){
|
|
||||||
vmContains,
|
|
||||||
vmStartsWith,
|
|
||||||
vmEndsWith,
|
|
||||||
vmFormat,
|
|
||||||
vmJoin,
|
|
||||||
vmToJSON,
|
|
||||||
vmFromJSON,
|
|
||||||
vmAlways,
|
|
||||||
rc.vmCancelled(),
|
|
||||||
rc.vmSuccess(),
|
|
||||||
rc.vmFailure(),
|
|
||||||
rc.vmHashFiles(),
|
|
||||||
|
|
||||||
rc.vmGithub(),
|
|
||||||
rc.vmJob(),
|
|
||||||
rc.vmSteps(),
|
|
||||||
rc.vmRunner(),
|
|
||||||
|
|
||||||
rc.vmSecrets(),
|
|
||||||
rc.vmStrategy(),
|
|
||||||
rc.vmMatrix(),
|
|
||||||
rc.vmEnv(),
|
|
||||||
rc.vmNeeds(),
|
|
||||||
rc.vmInputs(),
|
|
||||||
}
|
|
||||||
vm := otto.New()
|
|
||||||
for _, configer := range configers {
|
|
||||||
configer(vm)
|
|
||||||
}
|
|
||||||
return vm
|
|
||||||
}
|
|
||||||
|
|
||||||
func vmContains(vm *otto.Otto) {
|
|
||||||
_ = vm.Set("contains", func(searchString interface{}, searchValue string) bool {
|
|
||||||
if searchStringString, ok := searchString.(string); ok {
|
|
||||||
return strings.Contains(strings.ToLower(searchStringString), strings.ToLower(searchValue))
|
|
||||||
} else if searchStringArray, ok := searchString.([]string); ok {
|
|
||||||
for _, s := range searchStringArray {
|
|
||||||
if strings.EqualFold(s, searchValue) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func vmStartsWith(vm *otto.Otto) {
|
|
||||||
_ = vm.Set("startsWith", func(searchString string, searchValue string) bool {
|
|
||||||
return strings.HasPrefix(strings.ToLower(searchString), strings.ToLower(searchValue))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func vmEndsWith(vm *otto.Otto) {
|
|
||||||
_ = vm.Set("endsWith", func(searchString string, searchValue string) bool {
|
|
||||||
return strings.HasSuffix(strings.ToLower(searchString), strings.ToLower(searchValue))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func vmFormat(vm *otto.Otto) {
|
|
||||||
_ = vm.Set("format", func(s string, vals ...otto.Value) string {
|
|
||||||
ex := regexp.MustCompile(`(\{[0-9]+\}|\{.?|\}.?)`)
|
|
||||||
return ex.ReplaceAllStringFunc(s, func(seg string) string {
|
|
||||||
switch seg {
|
|
||||||
case "{{":
|
|
||||||
return "{"
|
|
||||||
case "}}":
|
|
||||||
return "}"
|
|
||||||
default:
|
|
||||||
if len(seg) < 3 || !strings.HasPrefix(seg, "{") {
|
|
||||||
log.Errorf("The following format string is invalid: '%v'", s)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
_i := seg[1 : len(seg)-1]
|
|
||||||
i, err := strconv.ParseInt(_i, 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("The following format string is invalid: '%v'. Error: %v", s, err)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if i >= int64(len(vals)) {
|
|
||||||
log.Errorf("The following format string references more arguments than were supplied: '%v'", s)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if vals[i].IsNull() || vals[i].IsUndefined() {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return vals[i].String()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func vmJoin(vm *otto.Otto) {
|
|
||||||
_ = vm.Set("join", func(element interface{}, optionalElem string) string {
|
|
||||||
slist := make([]string, 0)
|
|
||||||
if elementString, ok := element.(string); ok {
|
|
||||||
slist = append(slist, elementString)
|
|
||||||
} else if elementArray, ok := element.([]string); ok {
|
|
||||||
slist = append(slist, elementArray...)
|
|
||||||
}
|
|
||||||
if optionalElem != "" {
|
|
||||||
slist = append(slist, optionalElem)
|
|
||||||
}
|
|
||||||
return strings.Join(slist, " ")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func vmToJSON(vm *otto.Otto) {
|
|
||||||
toJSON := func(o interface{}) string {
|
|
||||||
rtn, err := json.MarshalIndent(o, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Unable to marshal: %v", err)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return string(rtn)
|
|
||||||
}
|
|
||||||
_ = vm.Set("toJSON", toJSON)
|
|
||||||
_ = vm.Set("toJson", toJSON)
|
|
||||||
}
|
|
||||||
|
|
||||||
func vmFromJSON(vm *otto.Otto) {
|
|
||||||
fromJSON := func(str string) interface{} {
|
|
||||||
var dat interface{}
|
|
||||||
err := json.Unmarshal([]byte(str), &dat)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Unable to unmarshal: %v", err)
|
|
||||||
return dat
|
|
||||||
}
|
|
||||||
return dat
|
|
||||||
}
|
|
||||||
_ = vm.Set("fromJSON", fromJSON)
|
|
||||||
_ = vm.Set("fromJson", fromJSON)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *RunContext) vmHashFiles() func(*otto.Otto) {
|
|
||||||
return func(vm *otto.Otto) {
|
|
||||||
_ = vm.Set("hashFiles", func(paths ...string) string {
|
|
||||||
var files []string
|
|
||||||
for i := range paths {
|
|
||||||
newFiles, err := filepath.Glob(filepath.Join(rc.Config.Workdir, paths[i]))
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Unable to glob.Glob: %v", err)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
files = append(files, newFiles...)
|
|
||||||
}
|
|
||||||
hasher := sha256.New()
|
|
||||||
for _, file := range files {
|
|
||||||
f, err := os.Open(file)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Unable to os.Open: %v", err)
|
|
||||||
}
|
|
||||||
if _, err := io.Copy(hasher, f); err != nil {
|
|
||||||
log.Errorf("Unable to io.Copy: %v", err)
|
|
||||||
}
|
|
||||||
if err := f.Close(); err != nil {
|
|
||||||
log.Errorf("Unable to Close file: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return hex.EncodeToString(hasher.Sum(nil))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *RunContext) vmSuccess() func(*otto.Otto) {
|
|
||||||
return func(vm *otto.Otto) {
|
|
||||||
_ = vm.Set("success", func() bool {
|
|
||||||
jobs := rc.Run.Workflow.Jobs
|
|
||||||
jobNeeds := rc.getNeedsTransitive(rc.Run.Job())
|
|
||||||
|
|
||||||
for _, needs := range jobNeeds {
|
|
||||||
if jobs[needs].Result != "success" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *RunContext) vmFailure() func(*otto.Otto) {
|
|
||||||
return func(vm *otto.Otto) {
|
|
||||||
_ = vm.Set("failure", func() bool {
|
|
||||||
jobs := rc.Run.Workflow.Jobs
|
|
||||||
jobNeeds := rc.getNeedsTransitive(rc.Run.Job())
|
|
||||||
|
|
||||||
for _, needs := range jobNeeds {
|
|
||||||
if jobs[needs].Result == "failure" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func vmAlways(vm *otto.Otto) {
|
|
||||||
_ = vm.Set("always", func() bool {
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
func (rc *RunContext) vmCancelled() func(vm *otto.Otto) {
|
|
||||||
return func(vm *otto.Otto) {
|
|
||||||
_ = vm.Set("cancelled", func() bool {
|
|
||||||
return rc.getJobContext().Status == "cancelled"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *RunContext) vmGithub() func(*otto.Otto) {
|
|
||||||
github := rc.getGithubContext()
|
|
||||||
|
|
||||||
return func(vm *otto.Otto) {
|
|
||||||
_ = vm.Set("github", github)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *RunContext) vmEnv() func(*otto.Otto) {
|
|
||||||
return func(vm *otto.Otto) {
|
|
||||||
env := rc.GetEnv()
|
|
||||||
log.Debugf("context env => %v", env)
|
|
||||||
_ = vm.Set("env", env)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sc *StepContext) vmEnv() func(*otto.Otto) {
|
|
||||||
return func(vm *otto.Otto) {
|
|
||||||
log.Debugf("context env => %v", sc.Env)
|
|
||||||
_ = vm.Set("env", sc.Env)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *RunContext) vmInputs() func(*otto.Otto) {
|
|
||||||
return func(vm *otto.Otto) {
|
|
||||||
_ = vm.Set("inputs", rc.Inputs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sc *StepContext) vmNeeds() func(*otto.Otto) {
|
|
||||||
jobs := sc.RunContext.Run.Workflow.Jobs
|
|
||||||
jobNeeds := sc.RunContext.Run.Job().Needs()
|
|
||||||
|
|
||||||
using := make(map[string]map[string]map[string]string)
|
using := make(map[string]map[string]map[string]string)
|
||||||
for _, needs := range jobNeeds {
|
for _, needs := range jobNeeds {
|
||||||
|
@ -463,182 +36,225 @@ func (sc *StepContext) vmNeeds() func(*otto.Otto) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return func(vm *otto.Otto) {
|
secrets := rc.Config.Secrets
|
||||||
log.Debugf("context needs => %v", using)
|
if rc.Composite != nil {
|
||||||
_ = vm.Set("needs", using)
|
secrets = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ee := &exprparser.EvaluationEnvironment{
|
||||||
|
Github: rc.getGithubContext(),
|
||||||
|
Env: rc.GetEnv(),
|
||||||
|
Job: rc.getJobContext(),
|
||||||
|
// todo: should be unavailable
|
||||||
|
// but required to interpolate/evaluate the step outputs on the job
|
||||||
|
Steps: rc.getStepsContext(),
|
||||||
|
Runner: map[string]interface{}{
|
||||||
|
"os": "Linux",
|
||||||
|
"temp": "/tmp",
|
||||||
|
"tool_cache": "/opt/hostedtoolcache",
|
||||||
|
},
|
||||||
|
Secrets: secrets,
|
||||||
|
Strategy: strategy,
|
||||||
|
Matrix: rc.Matrix,
|
||||||
|
Needs: using,
|
||||||
|
Inputs: rc.Inputs,
|
||||||
|
}
|
||||||
|
return expressionEvaluator{
|
||||||
|
interpreter: exprparser.NewInterpeter(ee, exprparser.Config{
|
||||||
|
Run: rc.Run,
|
||||||
|
WorkingDir: rc.Config.Workdir,
|
||||||
|
Context: "job",
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *StepContext) vmSuccess() func(*otto.Otto) {
|
// NewExpressionEvaluator creates a new evaluator
|
||||||
return func(vm *otto.Otto) {
|
func (sc *StepContext) NewExpressionEvaluator() ExpressionEvaluator {
|
||||||
_ = vm.Set("success", func() bool {
|
rc := sc.RunContext
|
||||||
return sc.RunContext.getJobContext().Status == "success"
|
// todo: cleanup EvaluationEnvironment creation
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sc *StepContext) vmFailure() func(*otto.Otto) {
|
|
||||||
return func(vm *otto.Otto) {
|
|
||||||
_ = vm.Set("failure", func() bool {
|
|
||||||
return sc.RunContext.getJobContext().Status == "failure"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type vmNeedsStruct struct {
|
|
||||||
Outputs map[string]string `json:"outputs"`
|
|
||||||
Result string `json:"result"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *RunContext) vmNeeds() func(*otto.Otto) {
|
|
||||||
return func(vm *otto.Otto) {
|
|
||||||
needsFunc := func() otto.Value {
|
|
||||||
jobs := rc.Run.Workflow.Jobs
|
|
||||||
jobNeeds := rc.Run.Job().Needs()
|
|
||||||
|
|
||||||
using := make(map[string]vmNeedsStruct)
|
|
||||||
for _, needs := range jobNeeds {
|
|
||||||
using[needs] = vmNeedsStruct{
|
|
||||||
Outputs: jobs[needs].Outputs,
|
|
||||||
Result: jobs[needs].Result,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("context needs => %+v", using)
|
|
||||||
|
|
||||||
value, err := vm.ToValue(using)
|
|
||||||
if err != nil {
|
|
||||||
return vm.MakeTypeError(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Results might change after the Otto VM was created
|
|
||||||
// and initialized. To access the current state
|
|
||||||
// we can't just pass a copy to Otto - instead we
|
|
||||||
// created a 'live-binding'.
|
|
||||||
// Technical Note: We don't want to pollute the global
|
|
||||||
// js namespace (and add things github actions hasn't)
|
|
||||||
// we delete the helper function after installing it
|
|
||||||
// as a getter.
|
|
||||||
global, _ := vm.Run("this")
|
|
||||||
_ = global.Object().Set("__needs__", needsFunc)
|
|
||||||
_, _ = vm.Run(`
|
|
||||||
(function (global) {
|
|
||||||
Object.defineProperty(global, 'needs', { get: global.__needs__ });
|
|
||||||
delete global.__needs__;
|
|
||||||
})(this)
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *RunContext) vmJob() func(*otto.Otto) {
|
|
||||||
job := rc.getJobContext()
|
|
||||||
|
|
||||||
return func(vm *otto.Otto) {
|
|
||||||
_ = vm.Set("job", job)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *RunContext) vmSteps() func(*otto.Otto) {
|
|
||||||
ctxSteps := rc.getStepsContext()
|
|
||||||
|
|
||||||
steps := make(map[string]interface{})
|
|
||||||
for id, ctxStep := range ctxSteps {
|
|
||||||
steps[id] = map[string]interface{}{
|
|
||||||
"conclusion": ctxStep.Conclusion.String(),
|
|
||||||
"outcome": ctxStep.Outcome.String(),
|
|
||||||
"outputs": ctxStep.Outputs,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return func(vm *otto.Otto) {
|
|
||||||
log.Debugf("context steps => %v", steps)
|
|
||||||
_ = vm.Set("steps", steps)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *RunContext) vmRunner() func(*otto.Otto) {
|
|
||||||
runner := map[string]interface{}{
|
|
||||||
"os": "Linux",
|
|
||||||
"temp": "/tmp",
|
|
||||||
"tool_cache": "/opt/hostedtoolcache",
|
|
||||||
}
|
|
||||||
|
|
||||||
return func(vm *otto.Otto) {
|
|
||||||
_ = vm.Set("runner", runner)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *RunContext) vmSecrets() func(*otto.Otto) {
|
|
||||||
return func(vm *otto.Otto) {
|
|
||||||
// Hide secrets from composite actions
|
|
||||||
if rc.Composite == nil {
|
|
||||||
_ = vm.Set("secrets", rc.Config.Secrets)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *RunContext) vmStrategy() func(*otto.Otto) {
|
|
||||||
job := rc.Run.Job()
|
job := rc.Run.Job()
|
||||||
strategy := make(map[string]interface{})
|
strategy := make(map[string]interface{})
|
||||||
if job.Strategy != nil {
|
if job.Strategy != nil {
|
||||||
strategy["fail-fast"] = job.Strategy.FailFast
|
strategy["fail-fast"] = job.Strategy.FailFast
|
||||||
strategy["max-parallel"] = job.Strategy.MaxParallel
|
strategy["max-parallel"] = job.Strategy.MaxParallel
|
||||||
}
|
}
|
||||||
return func(vm *otto.Otto) {
|
|
||||||
_ = vm.Set("strategy", strategy)
|
jobs := rc.Run.Workflow.Jobs
|
||||||
|
jobNeeds := rc.Run.Job().Needs()
|
||||||
|
|
||||||
|
using := make(map[string]map[string]map[string]string)
|
||||||
|
for _, needs := range jobNeeds {
|
||||||
|
using[needs] = map[string]map[string]string{
|
||||||
|
"outputs": jobs[needs].Outputs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets := rc.Config.Secrets
|
||||||
|
if rc.Composite != nil {
|
||||||
|
secrets = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ee := &exprparser.EvaluationEnvironment{
|
||||||
|
Github: rc.getGithubContext(),
|
||||||
|
Env: rc.GetEnv(),
|
||||||
|
Job: rc.getJobContext(),
|
||||||
|
Steps: rc.getStepsContext(),
|
||||||
|
Runner: map[string]interface{}{
|
||||||
|
"os": "Linux",
|
||||||
|
"temp": "/tmp",
|
||||||
|
"tool_cache": "/opt/hostedtoolcache",
|
||||||
|
},
|
||||||
|
Secrets: secrets,
|
||||||
|
Strategy: strategy,
|
||||||
|
Matrix: rc.Matrix,
|
||||||
|
Needs: using,
|
||||||
|
// todo: should be unavailable
|
||||||
|
// but required to interpolate/evaluate the inputs in actions/composite
|
||||||
|
Inputs: rc.Inputs,
|
||||||
|
}
|
||||||
|
return expressionEvaluator{
|
||||||
|
interpreter: exprparser.NewInterpeter(ee, exprparser.Config{
|
||||||
|
Run: rc.Run,
|
||||||
|
WorkingDir: rc.Config.Workdir,
|
||||||
|
Context: "step",
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rc *RunContext) vmMatrix() func(*otto.Otto) {
|
type expressionEvaluator struct {
|
||||||
return func(vm *otto.Otto) {
|
interpreter exprparser.Interpreter
|
||||||
_ = vm.Set("matrix", rc.Matrix)
|
}
|
||||||
|
|
||||||
|
func (ee expressionEvaluator) evaluate(in string, isIfExpression bool) (interface{}, error) {
|
||||||
|
evaluated, err := ee.interpreter.Evaluate(in, isIfExpression)
|
||||||
|
return evaluated, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ee expressionEvaluator) Interpolate(in string) string {
|
||||||
|
if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") {
|
||||||
|
return in
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expr, _ := rewriteSubExpression(in, true)
|
||||||
|
if in != expr {
|
||||||
|
log.Debugf("expression '%s' rewritten to '%s'", in, expr)
|
||||||
|
}
|
||||||
|
|
||||||
|
evaluated, err := ee.evaluate(expr, false)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Unable to interpolate expression '%s': %s", expr, err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("expression '%s' evaluated to '%s'", expr, evaluated)
|
||||||
|
|
||||||
|
value, ok := evaluated.(string)
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("Expression %s did not evaluate to a string", expr))
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
// EvalBool evaluates an expression against given evaluator
|
// EvalBool evaluates an expression against given evaluator
|
||||||
func EvalBool(evaluator ExpressionEvaluator, expr string) (bool, error) {
|
func EvalBool(evaluator ExpressionEvaluator, expr string) (bool, error) {
|
||||||
if splitPattern == nil {
|
nextExpr, _ := rewriteSubExpression(expr, false)
|
||||||
splitPattern = regexp.MustCompile(fmt.Sprintf(`%s|%s|\S+`, expressionPattern.String(), operatorPattern.String()))
|
if expr != nextExpr {
|
||||||
|
log.Debugf("expression '%s' rewritten to '%s'", expr, nextExpr)
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(strings.TrimSpace(expr), "!") {
|
|
||||||
return false, errors.New("expressions starting with ! must be wrapped in ${{ }}")
|
evaluated, err := evaluator.evaluate(nextExpr, true)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
}
|
}
|
||||||
if expr != "" {
|
|
||||||
parts := splitPattern.FindAllString(expr, -1)
|
|
||||||
var evaluatedParts []string
|
|
||||||
for i, part := range parts {
|
|
||||||
if operatorPattern.MatchString(part) {
|
|
||||||
evaluatedParts = append(evaluatedParts, part)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
interpolatedPart, isString := evaluator.InterpolateWithStringCheck(part)
|
var result bool
|
||||||
|
|
||||||
// This peculiar transformation has to be done because the GitHub parser
|
switch t := evaluated.(type) {
|
||||||
// treats false returned from contexts as a string, not a boolean.
|
case bool:
|
||||||
// Hence env.SOMETHING will be evaluated to true in an if: expression
|
result = t
|
||||||
// regardless if SOMETHING is set to false, true or any other string.
|
case string:
|
||||||
// It also handles some other weirdness that I found by trial and error.
|
result = t != ""
|
||||||
if (expressionPattern.MatchString(part) && // it is an expression
|
case int:
|
||||||
!strings.Contains(part, "!")) && // but it's not negated
|
result = t != 0
|
||||||
interpolatedPart == "false" && // and the interpolated string is false
|
case float64:
|
||||||
(isString || previousOrNextPartIsAnOperator(i, parts)) { // and it's of type string or has an logical operator before or after
|
if math.IsNaN(t) {
|
||||||
interpolatedPart = fmt.Sprintf("'%s'", interpolatedPart) // then we have to quote the false expression
|
result = false
|
||||||
}
|
} else {
|
||||||
|
result = t != 0
|
||||||
evaluatedParts = append(evaluatedParts, interpolatedPart)
|
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
joined := strings.Join(evaluatedParts, " ")
|
return false, fmt.Errorf("Unable to map return type to boolean for '%s'", expr)
|
||||||
v, _, err := evaluator.Evaluate(fmt.Sprintf("Boolean(%s)", joined))
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
log.Debugf("expression '%s' evaluated to '%s'", expr, v)
|
|
||||||
return v == "true", nil
|
|
||||||
}
|
}
|
||||||
return true, nil
|
|
||||||
|
log.Debugf("expression '%s' evaluated to '%t'", nextExpr, result)
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func escapeFormatString(in string) string {
|
||||||
|
return strings.ReplaceAll(strings.ReplaceAll(in, "{", "{{"), "}", "}}")
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:gocyclo
|
||||||
|
func rewriteSubExpression(in string, forceFormat bool) (string, error) {
|
||||||
|
if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") {
|
||||||
|
return in, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
strPattern := regexp.MustCompile("(?:''|[^'])*'")
|
||||||
|
pos := 0
|
||||||
|
exprStart := -1
|
||||||
|
strStart := -1
|
||||||
|
var results []string
|
||||||
|
formatOut := ""
|
||||||
|
for pos < len(in) {
|
||||||
|
if strStart > -1 {
|
||||||
|
matches := strPattern.FindStringIndex(in[pos:])
|
||||||
|
if matches == nil {
|
||||||
|
panic("unclosed string.")
|
||||||
|
}
|
||||||
|
|
||||||
|
strStart = -1
|
||||||
|
pos += matches[1]
|
||||||
|
} else if exprStart > -1 {
|
||||||
|
exprEnd := strings.Index(in[pos:], "}}")
|
||||||
|
strStart = strings.Index(in[pos:], "'")
|
||||||
|
|
||||||
|
if exprEnd > -1 && strStart > -1 {
|
||||||
|
if exprEnd < strStart {
|
||||||
|
strStart = -1
|
||||||
|
} else {
|
||||||
|
exprEnd = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if exprEnd > -1 {
|
||||||
|
formatOut += fmt.Sprintf("{%d}", len(results))
|
||||||
|
results = append(results, strings.TrimSpace(in[exprStart:pos+exprEnd]))
|
||||||
|
pos += exprEnd + 2
|
||||||
|
exprStart = -1
|
||||||
|
} else if strStart > -1 {
|
||||||
|
pos += strStart + 1
|
||||||
|
} else {
|
||||||
|
panic("unclosed expression.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
exprStart = strings.Index(in[pos:], "${{")
|
||||||
|
if exprStart != -1 {
|
||||||
|
formatOut += escapeFormatString(in[pos : pos+exprStart])
|
||||||
|
exprStart = pos + exprStart + 3
|
||||||
|
pos = exprStart
|
||||||
|
} else {
|
||||||
|
formatOut += escapeFormatString(in[pos:])
|
||||||
|
pos = len(in)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) == 1 && formatOut == "{0}" && !forceFormat {
|
||||||
|
return in, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("format('%s', %s)", strings.ReplaceAll(formatOut, "'", "''"), strings.Join(results, ", ")), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import (
|
||||||
yaml "gopkg.in/yaml.v3"
|
yaml "gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEvaluate(t *testing.T) {
|
func createRunContext(t *testing.T) *RunContext {
|
||||||
var yml yaml.Node
|
var yml yaml.Node
|
||||||
err := yml.Encode(map[string][]interface{}{
|
err := yml.Encode(map[string][]interface{}{
|
||||||
"os": {"Linux", "Windows"},
|
"os": {"Linux", "Windows"},
|
||||||
|
@ -20,7 +20,7 @@ func TestEvaluate(t *testing.T) {
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
rc := &RunContext{
|
return &RunContext{
|
||||||
Config: &Config{
|
Config: &Config{
|
||||||
Workdir: ".",
|
Workdir: ".",
|
||||||
Secrets: map[string]string{
|
Secrets: map[string]string{
|
||||||
|
@ -71,54 +71,50 @@ func TestEvaluate(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEvaluateRunContext(t *testing.T) {
|
||||||
|
rc := createRunContext(t)
|
||||||
ee := rc.NewExpressionEvaluator()
|
ee := rc.NewExpressionEvaluator()
|
||||||
|
|
||||||
tables := []struct {
|
tables := []struct {
|
||||||
in string
|
in string
|
||||||
out string
|
out interface{}
|
||||||
errMesg string
|
errMesg string
|
||||||
}{
|
}{
|
||||||
{" 1 ", "1", ""},
|
{" 1 ", 1, ""},
|
||||||
{"1 + 3", "4", ""},
|
// {"1 + 3", "4", ""},
|
||||||
{"(1 + 3) * -2", "-8", ""},
|
// {"(1 + 3) * -2", "-8", ""},
|
||||||
{"'my text'", "my text", ""},
|
{"'my text'", "my text", ""},
|
||||||
{"contains('my text', 'te')", "true", ""},
|
{"contains('my text', 'te')", true, ""},
|
||||||
{"contains('my TEXT', 'te')", "true", ""},
|
{"contains('my TEXT', 'te')", true, ""},
|
||||||
{"contains(['my text'], 'te')", "false", ""},
|
{"contains(fromJSON('[\"my text\"]'), 'te')", false, ""},
|
||||||
{"contains(['foo','bar'], 'bar')", "true", ""},
|
{"contains(fromJSON('[\"foo\",\"bar\"]'), 'bar')", true, ""},
|
||||||
{"startsWith('hello world', 'He')", "true", ""},
|
{"startsWith('hello world', 'He')", true, ""},
|
||||||
{"endsWith('hello world', 'ld')", "true", ""},
|
{"endsWith('hello world', 'ld')", true, ""},
|
||||||
{"format('0:{0} 2:{2} 1:{1}', 'zero', 'one', 'two')", "0:zero 2:two 1:one", ""},
|
{"format('0:{0} 2:{2} 1:{1}', 'zero', 'one', 'two')", "0:zero 2:two 1:one", ""},
|
||||||
{"join(['hello'],'octocat')", "hello octocat", ""},
|
{"join(fromJSON('[\"hello\"]'),'octocat')", "hello", ""},
|
||||||
{"join(['hello','mona','the'],'octocat')", "hello mona the octocat", ""},
|
{"join(fromJSON('[\"hello\",\"mona\",\"the\"]'),'octocat')", "hellooctocatmonaoctocatthe", ""},
|
||||||
{"join('hello','mona')", "hello mona", ""},
|
{"join('hello','mona')", "hello", ""},
|
||||||
{"toJSON({'foo':'bar'})", "{\n \"foo\": \"bar\"\n}", ""},
|
{"toJSON(env)", "{\n \"ACT\": \"true\",\n \"key\": \"value\"\n}", ""},
|
||||||
{"toJson({'foo':'bar'})", "{\n \"foo\": \"bar\"\n}", ""},
|
{"toJson(env)", "{\n \"ACT\": \"true\",\n \"key\": \"value\"\n}", ""},
|
||||||
{"(fromJSON('{\"foo\":\"bar\"}')).foo", "bar", ""},
|
{"(fromJSON('{\"foo\":\"bar\"}')).foo", "bar", ""},
|
||||||
{"(fromJson('{\"foo\":\"bar\"}')).foo", "bar", ""},
|
{"(fromJson('{\"foo\":\"bar\"}')).foo", "bar", ""},
|
||||||
{"(fromJson('[\"foo\",\"bar\"]'))[1]", "bar", ""},
|
{"(fromJson('[\"foo\",\"bar\"]'))[1]", "bar", ""},
|
||||||
{"hashFiles('**/non-extant-files')", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", ""},
|
// github does return an empty string for non-existent files
|
||||||
{"hashFiles('**/non-extant-files', '**/more-non-extant-files')", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", ""},
|
{"hashFiles('**/non-extant-files')", "", ""},
|
||||||
{"hashFiles('**/non.extant.files')", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", ""},
|
{"hashFiles('**/non-extant-files', '**/more-non-extant-files')", "", ""},
|
||||||
{"hashFiles('**/non''extant''files')", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", ""},
|
{"hashFiles('**/non.extant.files')", "", ""},
|
||||||
{"success()", "true", ""},
|
{"hashFiles('**/non''extant''files')", "", ""},
|
||||||
{"failure()", "false", ""},
|
{"success()", true, ""},
|
||||||
{"always()", "true", ""},
|
{"failure()", false, ""},
|
||||||
{"cancelled()", "false", ""},
|
{"always()", true, ""},
|
||||||
|
{"cancelled()", false, ""},
|
||||||
{"github.workflow", "test-workflow", ""},
|
{"github.workflow", "test-workflow", ""},
|
||||||
{"github.actor", "nektos/act", ""},
|
{"github.actor", "nektos/act", ""},
|
||||||
{"github.run_id", "1", ""},
|
{"github.run_id", "1", ""},
|
||||||
{"github.run_number", "1", ""},
|
{"github.run_number", "1", ""},
|
||||||
{"job.status", "success", ""},
|
{"job.status", "success", ""},
|
||||||
{"steps.idwithnothing.conclusion", "success", ""},
|
|
||||||
{"steps.idwithnothing.outcome", "failure", ""},
|
|
||||||
{"steps.idwithnothing.outputs.foowithnothing", "barwithnothing", ""},
|
|
||||||
{"steps.id-with-hyphens.conclusion", "success", ""},
|
|
||||||
{"steps.id-with-hyphens.outcome", "failure", ""},
|
|
||||||
{"steps.id-with-hyphens.outputs.foo-with-hyphens", "bar-with-hyphens", ""},
|
|
||||||
{"steps.id_with_underscores.conclusion", "success", ""},
|
|
||||||
{"steps.id_with_underscores.outcome", "failure", ""},
|
|
||||||
{"steps.id_with_underscores.outputs.foo_with_underscores", "bar_with_underscores", ""},
|
|
||||||
{"runner.os", "Linux", ""},
|
{"runner.os", "Linux", ""},
|
||||||
{"matrix.os", "Linux", ""},
|
{"matrix.os", "Linux", ""},
|
||||||
{"matrix.foo", "bar", ""},
|
{"matrix.foo", "bar", ""},
|
||||||
|
@ -139,7 +135,47 @@ func TestEvaluate(t *testing.T) {
|
||||||
table := table
|
table := table
|
||||||
t.Run(table.in, func(t *testing.T) {
|
t.Run(table.in, func(t *testing.T) {
|
||||||
assertObject := assert.New(t)
|
assertObject := assert.New(t)
|
||||||
out, _, err := ee.Evaluate(table.in)
|
out, err := ee.evaluate(table.in, false)
|
||||||
|
if table.errMesg == "" {
|
||||||
|
assertObject.NoError(err, table.in)
|
||||||
|
assertObject.Equal(table.out, out, table.in)
|
||||||
|
} else {
|
||||||
|
assertObject.Error(err, table.in)
|
||||||
|
assertObject.Equal(table.errMesg, err.Error(), table.in)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEvaluateStepContext(t *testing.T) {
|
||||||
|
rc := createRunContext(t)
|
||||||
|
|
||||||
|
sc := &StepContext{
|
||||||
|
RunContext: rc,
|
||||||
|
}
|
||||||
|
ee := sc.NewExpressionEvaluator()
|
||||||
|
|
||||||
|
tables := []struct {
|
||||||
|
in string
|
||||||
|
out interface{}
|
||||||
|
errMesg string
|
||||||
|
}{
|
||||||
|
{"steps.idwithnothing.conclusion", model.StepStatusSuccess, ""},
|
||||||
|
{"steps.idwithnothing.outcome", model.StepStatusFailure, ""},
|
||||||
|
{"steps.idwithnothing.outputs.foowithnothing", "barwithnothing", ""},
|
||||||
|
{"steps.id-with-hyphens.conclusion", model.StepStatusSuccess, ""},
|
||||||
|
{"steps.id-with-hyphens.outcome", model.StepStatusFailure, ""},
|
||||||
|
{"steps.id-with-hyphens.outputs.foo-with-hyphens", "bar-with-hyphens", ""},
|
||||||
|
{"steps.id_with_underscores.conclusion", model.StepStatusSuccess, ""},
|
||||||
|
{"steps.id_with_underscores.outcome", model.StepStatusFailure, ""},
|
||||||
|
{"steps.id_with_underscores.outputs.foo_with_underscores", "bar_with_underscores", ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, table := range tables {
|
||||||
|
table := table
|
||||||
|
t.Run(table.in, func(t *testing.T) {
|
||||||
|
assertObject := assert.New(t)
|
||||||
|
out, err := ee.evaluate(table.in, false)
|
||||||
if table.errMesg == "" {
|
if table.errMesg == "" {
|
||||||
assertObject.NoError(err, table.in)
|
assertObject.NoError(err, table.in)
|
||||||
assertObject.Equal(table.out, out, table.in)
|
assertObject.Equal(table.out, out, table.in)
|
||||||
|
@ -181,7 +217,12 @@ func TestInterpolate(t *testing.T) {
|
||||||
in string
|
in string
|
||||||
out string
|
out string
|
||||||
}{
|
}{
|
||||||
{" ${{1}} to ${{2}} ", " 1 to 2 "},
|
{" text ", " text "},
|
||||||
|
{" $text ", " $text "},
|
||||||
|
{" ${text} ", " ${text} "},
|
||||||
|
{" ${{ 1 }} to ${{2}} ", " 1 to 2 "},
|
||||||
|
{" ${{ (true || false) }} to ${{2}} ", " true to 2 "},
|
||||||
|
{" ${{ (false || '}}' ) }} to ${{2}} ", " }} to 2 "},
|
||||||
{" ${{ env.KEYWITHNOTHING }} ", " valuewithnothing "},
|
{" ${{ env.KEYWITHNOTHING }} ", " valuewithnothing "},
|
||||||
{" ${{ env.KEY-WITH-HYPHENS }} ", " value-with-hyphens "},
|
{" ${{ env.KEY-WITH-HYPHENS }} ", " value-with-hyphens "},
|
||||||
{" ${{ env.KEY_WITH_UNDERSCORES }} ", " value_with_underscores "},
|
{" ${{ env.KEY_WITH_UNDERSCORES }} ", " value_with_underscores "},
|
||||||
|
@ -205,12 +246,13 @@ func TestInterpolate(t *testing.T) {
|
||||||
{"${{ env.SOMETHING_TRUE || false }}", "true"},
|
{"${{ env.SOMETHING_TRUE || false }}", "true"},
|
||||||
{"${{ env.SOMETHING_FALSE || false }}", "false"},
|
{"${{ env.SOMETHING_FALSE || false }}", "false"},
|
||||||
{"${{ env.SOMETHING_FALSE }} && ${{ env.SOMETHING_TRUE }}", "false && true"},
|
{"${{ env.SOMETHING_FALSE }} && ${{ env.SOMETHING_TRUE }}", "false && true"},
|
||||||
|
{"${{ fromJSON('{}') < 2 }}", "false"},
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTestExpressionWorkflow(t, tables, rc)
|
updateTestExpressionWorkflow(t, tables, rc)
|
||||||
for _, table := range tables {
|
for _, table := range tables {
|
||||||
table := table
|
table := table
|
||||||
t.Run(table.in, func(t *testing.T) {
|
t.Run("interpolate", func(t *testing.T) {
|
||||||
assertObject := assert.New(t)
|
assertObject := assert.New(t)
|
||||||
out := ee.Interpolate(table.in)
|
out := ee.Interpolate(table.in)
|
||||||
assertObject.Equal(table.out, out, table.in)
|
assertObject.Equal(table.out, out, table.in)
|
||||||
|
@ -247,7 +289,7 @@ jobs:
|
||||||
`, envs)
|
`, envs)
|
||||||
// editorconfig-checker-enable
|
// editorconfig-checker-enable
|
||||||
for _, table := range tables {
|
for _, table := range tables {
|
||||||
expressionPattern = regexp.MustCompile(`\${{\s*(.+?)\s*}}`)
|
expressionPattern := regexp.MustCompile(`\${{\s*(.+?)\s*}}`)
|
||||||
|
|
||||||
expr := expressionPattern.ReplaceAllStringFunc(table.in, func(match string) string {
|
expr := expressionPattern.ReplaceAllStringFunc(table.in, func(match string) string {
|
||||||
return fmt.Sprintf("€{{ %s }}", expressionPattern.ReplaceAllString(match, "$1"))
|
return fmt.Sprintf("€{{ %s }}", expressionPattern.ReplaceAllString(match, "$1"))
|
||||||
|
@ -268,43 +310,56 @@ jobs:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRewrite(t *testing.T) {
|
func TestRewriteSubExpression(t *testing.T) {
|
||||||
rc := &RunContext{
|
table := []struct {
|
||||||
Config: &Config{},
|
in string
|
||||||
Run: &model.Run{
|
out string
|
||||||
JobID: "job1",
|
|
||||||
Workflow: &model.Workflow{
|
|
||||||
Jobs: map[string]*model.Job{
|
|
||||||
"job1": {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
ee := rc.NewExpressionEvaluator()
|
|
||||||
|
|
||||||
tables := []struct {
|
|
||||||
in string
|
|
||||||
re string
|
|
||||||
}{
|
}{
|
||||||
{"ecole", "ecole"},
|
{in: "Hello World", out: "Hello World"},
|
||||||
{"ecole.centrale", "ecole['centrale']"},
|
{in: "${{ true }}", out: "${{ true }}"},
|
||||||
{"ecole['centrale']", "ecole['centrale']"},
|
{in: "${{ true }} ${{ true }}", out: "format('{0} {1}', true, true)"},
|
||||||
{"ecole.centrale.paris", "ecole['centrale']['paris']"},
|
{in: "${{ true || false }} ${{ true && true }}", out: "format('{0} {1}', true || false, true && true)"},
|
||||||
{"ecole['centrale'].paris", "ecole['centrale']['paris']"},
|
{in: "${{ '}}' }}", out: "${{ '}}' }}"},
|
||||||
{"ecole.centrale['paris']", "ecole['centrale']['paris']"},
|
{in: "${{ '''}}''' }}", out: "${{ '''}}''' }}"},
|
||||||
{"ecole['centrale']['paris']", "ecole['centrale']['paris']"},
|
{in: "${{ '''' }}", out: "${{ '''' }}"},
|
||||||
{"ecole.centrale-paris", "ecole['centrale-paris']"},
|
{in: `${{ fromJSON('"}}"') }}`, out: `${{ fromJSON('"}}"') }}`},
|
||||||
{"ecole['centrale-paris']", "ecole['centrale-paris']"},
|
{in: `${{ fromJSON('"\"}}\""') }}`, out: `${{ fromJSON('"\"}}\""') }}`},
|
||||||
{"ecole.centrale_paris", "ecole['centrale_paris']"},
|
{in: `${{ fromJSON('"''}}"') }}`, out: `${{ fromJSON('"''}}"') }}`},
|
||||||
{"ecole['centrale_paris']", "ecole['centrale_paris']"},
|
{in: "Hello ${{ 'World' }}", out: "format('Hello {0}', 'World')"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, table := range tables {
|
for _, table := range table {
|
||||||
table := table
|
t.Run("TestRewriteSubExpression", func(t *testing.T) {
|
||||||
t.Run(table.in, func(t *testing.T) {
|
|
||||||
assertObject := assert.New(t)
|
assertObject := assert.New(t)
|
||||||
re := ee.Rewrite(table.in)
|
out, err := rewriteSubExpression(table.in, false)
|
||||||
assertObject.Equal(table.re, re, table.in)
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assertObject.Equal(table.out, out, table.in)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRewriteSubExpressionForceFormat(t *testing.T) {
|
||||||
|
table := []struct {
|
||||||
|
in string
|
||||||
|
out string
|
||||||
|
}{
|
||||||
|
{in: "Hello World", out: "Hello World"},
|
||||||
|
{in: "${{ true }}", out: "format('{0}', true)"},
|
||||||
|
{in: "${{ '}}' }}", out: "format('{0}', '}}')"},
|
||||||
|
{in: `${{ fromJSON('"}}"') }}`, out: `format('{0}', fromJSON('"}}"'))`},
|
||||||
|
{in: "Hello ${{ 'World' }}", out: "format('Hello {0}', 'World')"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, table := range table {
|
||||||
|
t.Run("TestRewriteSubExpressionForceFormat", func(t *testing.T) {
|
||||||
|
assertObject := assert.New(t)
|
||||||
|
out, err := rewriteSubExpression(table.in, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assertObject.Equal(table.out, out, table.in)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -426,19 +426,6 @@ func (rc *RunContext) isEnabled(ctx context.Context) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
var splitPattern *regexp.Regexp
|
|
||||||
|
|
||||||
func previousOrNextPartIsAnOperator(i int, parts []string) bool {
|
|
||||||
operator := false
|
|
||||||
if i > 0 {
|
|
||||||
operator = operatorPattern.MatchString(parts[i-1])
|
|
||||||
}
|
|
||||||
if i+1 < len(parts) {
|
|
||||||
operator = operator || operatorPattern.MatchString(parts[i+1])
|
|
||||||
}
|
|
||||||
return operator
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeMaps(maps ...map[string]string) map[string]string {
|
func mergeMaps(maps ...map[string]string) map[string]string {
|
||||||
rtnMap := make(map[string]string)
|
rtnMap := make(map[string]string)
|
||||||
for _, m := range maps {
|
for _, m := range maps {
|
||||||
|
@ -499,17 +486,6 @@ func (rc *RunContext) getStepsContext() map[string]*model.StepResult {
|
||||||
return rc.StepResults
|
return rc.StepResults
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rc *RunContext) getNeedsTransitive(job *model.Job) []string {
|
|
||||||
needs := job.Needs()
|
|
||||||
|
|
||||||
for _, need := range needs {
|
|
||||||
parentNeeds := rc.getNeedsTransitive(rc.Run.Workflow.GetJob(need))
|
|
||||||
needs = append(needs, parentNeeds...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return needs
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *RunContext) getGithubContext() *model.GithubContext {
|
func (rc *RunContext) getGithubContext() *model.GithubContext {
|
||||||
ghc := &model.GithubContext{
|
ghc := &model.GithubContext{
|
||||||
Event: make(map[string]interface{}),
|
Event: make(map[string]interface{}),
|
||||||
|
@ -784,12 +760,11 @@ func (rc *RunContext) handleCredentials() (username, password string, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
ee := rc.NewExpressionEvaluator()
|
ee := rc.NewExpressionEvaluator()
|
||||||
var ok bool
|
if username = ee.Interpolate(container.Credentials["username"]); username == "" {
|
||||||
if username, ok = ee.InterpolateWithStringCheck(container.Credentials["username"]); !ok {
|
|
||||||
err = fmt.Errorf("failed to interpolate container.credentials.username")
|
err = fmt.Errorf("failed to interpolate container.credentials.username")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if password, ok = ee.InterpolateWithStringCheck(container.Credentials["password"]); !ok {
|
if password = ee.Interpolate(container.Credentials["password"]); password == "" {
|
||||||
err = fmt.Errorf("failed to interpolate container.credentials.password")
|
err = fmt.Errorf("failed to interpolate container.credentials.password")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,14 +75,16 @@ func TestRunContext_EvalBool(t *testing.T) {
|
||||||
{in: "success()", out: true},
|
{in: "success()", out: true},
|
||||||
{in: "cancelled()", out: false},
|
{in: "cancelled()", out: false},
|
||||||
{in: "always()", out: true},
|
{in: "always()", out: true},
|
||||||
{in: "steps.id1.conclusion == 'success'", out: true},
|
// TODO: move to sc.NewExpressionEvaluator(), because "steps" context is not available here
|
||||||
{in: "steps.id1.conclusion != 'success'", out: false},
|
// {in: "steps.id1.conclusion == 'success'", out: true},
|
||||||
{in: "steps.id1.outcome == 'failure'", out: true},
|
// {in: "steps.id1.conclusion != 'success'", out: false},
|
||||||
{in: "steps.id1.outcome != 'failure'", out: false},
|
// {in: "steps.id1.outcome == 'failure'", out: true},
|
||||||
|
// {in: "steps.id1.outcome != 'failure'", out: false},
|
||||||
{in: "true", out: true},
|
{in: "true", out: true},
|
||||||
{in: "false", out: false},
|
{in: "false", out: false},
|
||||||
{in: "!true", wantErr: true},
|
// TODO: This does not throw an error, because the evaluator does not know if the expression is inside ${{ }} or not
|
||||||
{in: "!false", wantErr: true},
|
// {in: "!true", wantErr: true},
|
||||||
|
// {in: "!false", wantErr: true},
|
||||||
{in: "1 != 0", out: true},
|
{in: "1 != 0", out: true},
|
||||||
{in: "1 != 1", out: false},
|
{in: "1 != 1", out: false},
|
||||||
{in: "${{ 1 != 0 }}", out: true},
|
{in: "${{ 1 != 0 }}", out: true},
|
||||||
|
@ -100,14 +102,15 @@ func TestRunContext_EvalBool(t *testing.T) {
|
||||||
{in: "env.UNKNOWN == 'true'", out: false},
|
{in: "env.UNKNOWN == 'true'", out: false},
|
||||||
{in: "env.UNKNOWN", out: false},
|
{in: "env.UNKNOWN", out: false},
|
||||||
// Inline expressions
|
// Inline expressions
|
||||||
{in: "env.SOME_TEXT", out: true}, // this is because Boolean('text') is true in Javascript
|
{in: "env.SOME_TEXT", out: true},
|
||||||
{in: "env.SOME_TEXT == 'text'", out: true},
|
{in: "env.SOME_TEXT == 'text'", out: true},
|
||||||
{in: "env.SOMETHING_TRUE == 'true'", out: true},
|
{in: "env.SOMETHING_TRUE == 'true'", out: true},
|
||||||
{in: "env.SOMETHING_FALSE == 'true'", out: false},
|
{in: "env.SOMETHING_FALSE == 'true'", out: false},
|
||||||
{in: "env.SOMETHING_TRUE", out: true},
|
{in: "env.SOMETHING_TRUE", out: true},
|
||||||
{in: "env.SOMETHING_FALSE", out: true}, // this is because Boolean('text') is true in Javascript
|
{in: "env.SOMETHING_FALSE", out: true},
|
||||||
{in: "!env.SOMETHING_TRUE", wantErr: true},
|
// TODO: This does not throw an error, because the evaluator does not know if the expression is inside ${{ }} or not
|
||||||
{in: "!env.SOMETHING_FALSE", wantErr: true},
|
// {in: "!env.SOMETHING_TRUE", wantErr: true},
|
||||||
|
// {in: "!env.SOMETHING_FALSE", wantErr: true},
|
||||||
{in: "${{ !env.SOMETHING_TRUE }}", out: false},
|
{in: "${{ !env.SOMETHING_TRUE }}", out: false},
|
||||||
{in: "${{ !env.SOMETHING_FALSE }}", out: false},
|
{in: "${{ !env.SOMETHING_FALSE }}", out: false},
|
||||||
{in: "${{ ! env.SOMETHING_TRUE }}", out: false},
|
{in: "${{ ! env.SOMETHING_TRUE }}", out: false},
|
||||||
|
@ -123,7 +126,8 @@ func TestRunContext_EvalBool(t *testing.T) {
|
||||||
{in: "${{ env.SOMETHING_TRUE && true }}", out: true},
|
{in: "${{ env.SOMETHING_TRUE && true }}", out: true},
|
||||||
{in: "${{ env.SOMETHING_FALSE || true }}", out: true},
|
{in: "${{ env.SOMETHING_FALSE || true }}", out: true},
|
||||||
{in: "${{ env.SOMETHING_FALSE || false }}", out: true},
|
{in: "${{ env.SOMETHING_FALSE || false }}", out: true},
|
||||||
{in: "!env.SOMETHING_TRUE || true", wantErr: true},
|
// TODO: This does not throw an error, because the evaluator does not know if the expression is inside ${{ }} or not
|
||||||
|
// {in: "!env.SOMETHING_TRUE || true", wantErr: true},
|
||||||
{in: "${{ env.SOMETHING_TRUE == 'true'}}", out: true},
|
{in: "${{ env.SOMETHING_TRUE == 'true'}}", out: true},
|
||||||
{in: "${{ env.SOMETHING_FALSE == 'true'}}", out: false},
|
{in: "${{ env.SOMETHING_FALSE == 'true'}}", out: false},
|
||||||
{in: "${{ env.SOMETHING_FALSE == 'false'}}", out: true},
|
{in: "${{ env.SOMETHING_FALSE == 'false'}}", out: true},
|
||||||
|
@ -198,7 +202,7 @@ jobs:
|
||||||
if table.wantErr || strings.HasPrefix(table.in, "github.actor") {
|
if table.wantErr || strings.HasPrefix(table.in, "github.actor") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
expressionPattern = regexp.MustCompile(`\${{\s*(.+?)\s*}}`)
|
expressionPattern := regexp.MustCompile(`\${{\s*(.+?)\s*}}`)
|
||||||
|
|
||||||
expr := expressionPattern.ReplaceAllStringFunc(table.in, func(match string) string {
|
expr := expressionPattern.ReplaceAllStringFunc(table.in, func(match string) string {
|
||||||
return fmt.Sprintf("€{{ %s }}", expressionPattern.ReplaceAllString(match, "$1"))
|
return fmt.Sprintf("€{{ %s }}", expressionPattern.ReplaceAllString(match, "$1"))
|
||||||
|
|
Loading…
Reference in New Issue