Refactor expression evaluator to use parser from actionlint package (#908)

* feat: implement expression evaluator

Co-authored-by: Markus Wolf <[email protected]>
Co-authored-by: Philipp Hinrichsen <[email protected]>

* feat: integrate exprparser into act

Co-authored-by: Markus Wolf <[email protected]>
Co-authored-by: Philipp Hinrichsen <[email protected]>

* Escape { and }, do not fail on missing properties

* Fix empty inputs context

* fix: contains() comparison for complex values

Co-authored-by: Markus Wolf <[email protected]>
Co-authored-by: Philipp Hinrichsen <[email protected]>
Co-authored-by: Christopher Homberger <[email protected]>
This commit is contained in:
authored and GitHub committed 2022-01-21 08:07:20 -08:00
1 parent 518148d162
commit eba71f98fe
12 files changed
+2047 -708

No files matched your search

+1 -4
View File
@@ -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
) )
+18 -7
View File
@@ -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=
+277
View File
@@ -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
}
+245
View File
@@ -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)
}
})
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+1
View File
@@ -0,0 +1 @@
Hello
+1
View File
@@ -0,0 +1 @@
World!
+205 -589
View File
File diff suppressed because it is too large. Load diff
+124 -69
View File
@@ -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
}{ }{
{" text ", " text "},
{" $text ", " $text "},
{" ${text} ", " ${text} "},
{" ${{ 1 }} to ${{2}} ", " 1 to 2 "}, {" ${{ 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{},
Run: &model.Run{
JobID: "job1",
Workflow: &model.Workflow{
Jobs: map[string]*model.Job{
"job1": {},
},
},
},
}
ee := rc.NewExpressionEvaluator()
tables := []struct {
in string in string
re string out 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)
}) })
} }
} }
+2 -27
View File
@@ -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
} }
+16 -12
View File
@@ -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"))