From cb14f1fed5abc1491b7e89c8415976089d43aaa7 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Thu, 26 Jan 2023 15:33:45 -0800 Subject: [PATCH 01/57] Refactoring the linter into an opinionated reformatter --- lint-workflow/Pipfile | 2 + lint-workflow/Pipfile.lock | 208 ++++++++++++++------- lint-workflow/lint.py | 2 +- lint-workflow/src/__init__.py | 2 + lint-workflow/src/load.py | 22 +++ lint-workflow/src/models/__init__.py | 3 + lint-workflow/src/models/job.py | 19 ++ lint-workflow/src/models/step.py | 23 +++ lint-workflow/src/models/workflow.py | 10 + lint-workflow/src/rules.py | 43 +++++ lint-workflow/tests/conftest.py | 1 + lint-workflow/tests/context.py | 5 + lint-workflow/tests/fixtures/test.yml | 2 +- lint-workflow/tests/test_action_update.py | 2 + lint-workflow/tests/test_job.py | 19 ++ lint-workflow/tests/test_lint.py | 3 + lint-workflow/tests/test_load.py | 22 +++ lint-workflow/tests/test_main.py | 7 + lint-workflow/tests/test_step.py | 44 +++++ lint-workflow/tests/test_workflow.py | 25 +++ lint-workflow/tests/test_workflow_files.py | 2 + 21 files changed, 401 insertions(+), 65 deletions(-) create mode 100644 lint-workflow/src/__init__.py create mode 100644 lint-workflow/src/load.py create mode 100644 lint-workflow/src/models/__init__.py create mode 100644 lint-workflow/src/models/job.py create mode 100644 lint-workflow/src/models/step.py create mode 100644 lint-workflow/src/models/workflow.py create mode 100644 lint-workflow/src/rules.py create mode 100644 lint-workflow/tests/conftest.py create mode 100644 lint-workflow/tests/context.py create mode 100644 lint-workflow/tests/test_job.py create mode 100644 lint-workflow/tests/test_load.py create mode 100644 lint-workflow/tests/test_step.py create mode 100644 lint-workflow/tests/test_workflow.py diff --git a/lint-workflow/Pipfile b/lint-workflow/Pipfile index de96a7f7..968707a7 100644 --- a/lint-workflow/Pipfile +++ b/lint-workflow/Pipfile @@ -6,6 +6,8 @@ name = "pypi" [packages] pyyaml = "*" urllib3 = "*" +pydantic = "*" +"ruamel.yaml" = "*" [dev-packages] black = "*" diff --git a/lint-workflow/Pipfile.lock b/lint-workflow/Pipfile.lock index 748a2fef..54e9614b 100644 --- a/lint-workflow/Pipfile.lock +++ b/lint-workflow/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "bc7e46794f16595c77443451de92e409bd86605cdc1d788ee138dab0c5bc13d8" + "sha256": "0316d5bb4d819d8f0e3ef9b8c91dbbef236167a3d44b5d94f42ef70b2172a8aa" }, "pipfile-spec": 6, "requires": { @@ -16,6 +16,48 @@ ] }, "default": { + "pydantic": { + "hashes": [ + "sha256:05a81b006be15655b2a1bae5faa4280cf7c81d0e09fcb49b342ebf826abe5a72", + "sha256:0b53e1d41e97063d51a02821b80538053ee4608b9a181c1005441f1673c55423", + "sha256:2b3ce5f16deb45c472dde1a0ee05619298c864a20cded09c4edd820e1454129f", + "sha256:2e82a6d37a95e0b1b42b82ab340ada3963aea1317fd7f888bb6b9dfbf4fff57c", + "sha256:301d626a59edbe5dfb48fcae245896379a450d04baeed50ef40d8199f2733b06", + "sha256:39f4a73e5342b25c2959529f07f026ef58147249f9b7431e1ba8414a36761f53", + "sha256:4948f264678c703f3877d1c8877c4e3b2e12e549c57795107f08cf70c6ec7774", + "sha256:4b05697738e7d2040696b0a66d9f0a10bec0efa1883ca75ee9e55baf511909d6", + "sha256:51bdeb10d2db0f288e71d49c9cefa609bca271720ecd0c58009bd7504a0c464c", + "sha256:55b1625899acd33229c4352ce0ae54038529b412bd51c4915349b49ca575258f", + "sha256:572066051eeac73d23f95ba9a71349c42a3e05999d0ee1572b7860235b850cc6", + "sha256:6a05a9db1ef5be0fe63e988f9617ca2551013f55000289c671f71ec16f4985e3", + "sha256:6dc1cc241440ed7ca9ab59d9929075445da6b7c94ced281b3dd4cfe6c8cff817", + "sha256:6e7124d6855b2780611d9f5e1e145e86667eaa3bd9459192c8dc1a097f5e9903", + "sha256:75d52162fe6b2b55964fbb0af2ee58e99791a3138588c482572bb6087953113a", + "sha256:78cec42b95dbb500a1f7120bdf95c401f6abb616bbe8785ef09887306792e66e", + "sha256:7feb6a2d401f4d6863050f58325b8d99c1e56f4512d98b11ac64ad1751dc647d", + "sha256:8775d4ef5e7299a2f4699501077a0defdaac5b6c4321173bcb0f3c496fbadf85", + "sha256:887ca463c3bc47103c123bc06919c86720e80e1214aab79e9b779cda0ff92a00", + "sha256:9193d4f4ee8feca58bc56c8306bcb820f5c7905fd919e0750acdeeeef0615b28", + "sha256:983e720704431a6573d626b00662eb78a07148c9115129f9b4351091ec95ecc3", + "sha256:990406d226dea0e8f25f643b370224771878142155b879784ce89f633541a024", + "sha256:9cbdc268a62d9a98c56e2452d6c41c0263d64a2009aac69246486f01b4f594c4", + "sha256:a48f1953c4a1d9bd0b5167ac50da9a79f6072c63c4cef4cf2a3736994903583e", + "sha256:a9a6747cac06c2beb466064dda999a13176b23535e4c496c9d48e6406f92d42d", + "sha256:a9f2de23bec87ff306aef658384b02aa7c32389766af3c5dee9ce33e80222dfa", + "sha256:b5635de53e6686fe7a44b5cf25fcc419a0d5e5c1a1efe73d49d48fe7586db854", + "sha256:b6f9d649892a6f54a39ed56b8dfd5e08b5f3be5f893da430bed76975f3735d15", + "sha256:b9a3859f24eb4e097502a3be1fb4b2abb79b6103dd9e2e0edb70613a4459a648", + "sha256:cd8702c5142afda03dc2b1ee6bc358b62b3735b2cce53fc77b31ca9f728e4bc8", + "sha256:d7b5a3821225f5c43496c324b0d6875fde910a1c2933d726a743ce328fbb2a8c", + "sha256:d88c4c0e5c5dfd05092a4b271282ef0588e5f4aaf345778056fc5259ba098857", + "sha256:eb992a1ef739cc7b543576337bebfc62c0e6567434e522e97291b251a41dad7f", + "sha256:f2f7eb6273dd12472d7f218e1fef6f7c7c2f00ac2e1ecde4db8824c457300416", + "sha256:fdf88ab63c3ee282c76d652fc86518aacb737ff35796023fae56a65ced1a5978", + "sha256:fdf8d759ef326962b4678d89e275ffc55b7ce59d917d9f72233762061fd04a2d" + ], + "index": "pypi", + "version": "==1.10.4" + }, "pyyaml": { "hashes": [ "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", @@ -62,50 +104,97 @@ "index": "pypi", "version": "==6.0" }, + "ruamel.yaml": { + "hashes": [ + "sha256:742b35d3d665023981bd6d16b3d24248ce5df75fdb4e2924e93a05c1f8b61ca7", + "sha256:8b7ce697a2f212752a35c1ac414471dc16c424c9573be4926b56ff3f5d23b7af" + ], + "index": "pypi", + "version": "==0.17.21" + }, + "ruamel.yaml.clib": { + "hashes": [ + "sha256:045e0626baf1c52e5527bd5db361bc83180faaba2ff586e763d3d5982a876a9e", + "sha256:15910ef4f3e537eea7fe45f8a5d19997479940d9196f357152a09031c5be59f3", + "sha256:184faeaec61dbaa3cace407cffc5819f7b977e75360e8d5ca19461cd851a5fc5", + "sha256:1f08fd5a2bea9c4180db71678e850b995d2a5f4537be0e94557668cf0f5f9497", + "sha256:2aa261c29a5545adfef9296b7e33941f46aa5bbd21164228e833412af4c9c75f", + "sha256:3110a99e0f94a4a3470ff67fc20d3f96c25b13d24c6980ff841e82bafe827cac", + "sha256:3243f48ecd450eddadc2d11b5feb08aca941b5cd98c9b1db14b2fd128be8c697", + "sha256:370445fd795706fd291ab00c9df38a0caed0f17a6fb46b0f607668ecb16ce763", + "sha256:40d030e2329ce5286d6b231b8726959ebbe0404c92f0a578c0e2482182e38282", + "sha256:41d0f1fa4c6830176eef5b276af04c89320ea616655d01327d5ce65e50575c94", + "sha256:4a4d8d417868d68b979076a9be6a38c676eca060785abaa6709c7b31593c35d1", + "sha256:4b3a93bb9bc662fc1f99c5c3ea8e623d8b23ad22f861eb6fce9377ac07ad6072", + "sha256:5bc0667c1eb8f83a3752b71b9c4ba55ef7c7058ae57022dd9b29065186a113d9", + "sha256:721bc4ba4525f53f6a611ec0967bdcee61b31df5a56801281027a3a6d1c2daf5", + "sha256:763d65baa3b952479c4e972669f679fe490eee058d5aa85da483ebae2009d231", + "sha256:7bdb4c06b063f6fd55e472e201317a3bb6cdeeee5d5a38512ea5c01e1acbdd93", + "sha256:8831a2cedcd0f0927f788c5bdf6567d9dc9cc235646a434986a852af1cb54b4b", + "sha256:91a789b4aa0097b78c93e3dc4b40040ba55bef518f84a40d4442f713b4094acb", + "sha256:92460ce908546ab69770b2e576e4f99fbb4ce6ab4b245345a3869a0a0410488f", + "sha256:99e77daab5d13a48a4054803d052ff40780278240a902b880dd37a51ba01a307", + "sha256:a234a20ae07e8469da311e182e70ef6b199d0fbeb6c6cc2901204dd87fb867e8", + "sha256:a7b301ff08055d73223058b5c46c55638917f04d21577c95e00e0c4d79201a6b", + "sha256:be2a7ad8fd8f7442b24323d24ba0b56c51219513cfa45b9ada3b87b76c374d4b", + "sha256:bf9a6bc4a0221538b1a7de3ed7bca4c93c02346853f44e1cd764be0023cd3640", + "sha256:c3ca1fbba4ae962521e5eb66d72998b51f0f4d0f608d3c0347a48e1af262efa7", + "sha256:d000f258cf42fec2b1bbf2863c61d7b8918d31ffee905da62dede869254d3b8a", + "sha256:d5859983f26d8cd7bb5c287ef452e8aacc86501487634573d260968f753e1d71", + "sha256:d5e51e2901ec2366b79f16c2299a03e74ba4531ddcfacc1416639c557aef0ad8", + "sha256:debc87a9516b237d0466a711b18b6ebeb17ba9f391eb7f91c649c5c4ec5006c7", + "sha256:df5828871e6648db72d1c19b4bd24819b80a755c4541d3409f0f7acd0f335c80", + "sha256:ecdf1a604009bd35c674b9225a8fa609e0282d9b896c03dd441a91e5f53b534e", + "sha256:efa08d63ef03d079dcae1dfe334f6c8847ba8b645d08df286358b1f5293d24ab", + "sha256:f01da5790e95815eb5a8a138508c01c758e5f5bc0ce4286c4f7028b8dd7ac3d0", + "sha256:f34019dced51047d6f70cb9383b2ae2853b7fc4dce65129a5acd49f4f9256646" + ], + "markers": "python_version < '3.11' and platform_python_implementation == 'CPython'", + "version": "==0.2.7" + }, + "typing-extensions": { + "hashes": [ + "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", + "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" + ], + "markers": "python_version >= '3.7'", + "version": "==4.4.0" + }, "urllib3": { "hashes": [ - "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", - "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" + "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72", + "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1" ], "index": "pypi", - "version": "==1.26.12" + "version": "==1.26.14" } }, "develop": { "attrs": { "hashes": [ - "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6", - "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c" + "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836", + "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99" ], - "markers": "python_version >= '3.5'", - "version": "==22.1.0" + "markers": "python_version >= '3.6'", + "version": "==22.2.0" }, "black": { "hashes": [ - "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7", - "sha256:197df8509263b0b8614e1df1756b1dd41be6738eed2ba9e9769f3880c2b9d7b6", - "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650", - "sha256:2039230db3c6c639bd84efe3292ec7b06e9214a2992cd9beb293d639c6402edb", - "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d", - "sha256:2644b5d63633702bc2c5f3754b1b475378fbbfb481f62319388235d0cd104c2d", - "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de", - "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395", - "sha256:5b9b29da4f564ba8787c119f37d174f2b69cdfdf9015b7d8c5c16121ddc054ae", - "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa", - "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef", - "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383", - "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66", - "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87", - "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d", - "sha256:974308c58d057a651d182208a484ce80a26dac0caef2895836a92dd6ebd725e0", - "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b", - "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458", - "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4", - "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1", - "sha256:fba8a281e570adafb79f7755ac8721b6cf1bbf691186a287e990c7929c7692ff" + "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320", + "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351", + "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350", + "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f", + "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf", + "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148", + "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4", + "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d", + "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc", + "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d", + "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2", + "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f" ], "index": "pypi", - "version": "==22.10.0" + "version": "==22.12.0" }, "click": { "hashes": [ @@ -115,12 +204,21 @@ "markers": "python_version >= '3.7'", "version": "==8.1.3" }, + "exceptiongroup": { + "hashes": [ + "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e", + "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23" + ], + "markers": "python_version < '3.11'", + "version": "==1.1.0" + }, "iniconfig": { "hashes": [ - "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", - "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" ], - "version": "==1.1.1" + "markers": "python_version >= '3.7'", + "version": "==2.0.0" }, "mypy-extensions": { "hashes": [ @@ -131,27 +229,27 @@ }, "packaging": { "hashes": [ - "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", - "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" + "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2", + "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97" ], - "markers": "python_version >= '3.6'", - "version": "==21.3" + "markers": "python_version >= '3.7'", + "version": "==23.0" }, "pathspec": { "hashes": [ - "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93", - "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d" + "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6", + "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6" ], "markers": "python_version >= '3.7'", - "version": "==0.10.1" + "version": "==0.10.3" }, "platformdirs": { "hashes": [ - "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788", - "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19" + "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490", + "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2" ], "markers": "python_version >= '3.7'", - "version": "==2.5.2" + "version": "==2.6.2" }, "pluggy": { "hashes": [ @@ -161,29 +259,13 @@ "markers": "python_version >= '3.6'", "version": "==1.0.0" }, - "py": { - "hashes": [ - "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", - "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.11.0" - }, - "pyparsing": { - "hashes": [ - "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", - "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" - ], - "markers": "python_full_version >= '3.6.8'", - "version": "==3.0.9" - }, "pytest": { "hashes": [ - "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7", - "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39" + "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71", + "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59" ], "index": "pypi", - "version": "==7.1.3" + "version": "==7.2.0" }, "tomli": { "hashes": [ @@ -198,7 +280,7 @@ "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" ], - "markers": "python_version < '3.10'", + "markers": "python_version >= '3.7'", "version": "==4.4.0" } } diff --git a/lint-workflow/lint.py b/lint-workflow/lint.py index a43046f7..7e121fed 100644 --- a/lint-workflow/lint.py +++ b/lint-workflow/lint.py @@ -28,7 +28,7 @@ class Colors: white = "37m" -class LintFinding(object): +class LintFinding: """Represents a linting problem.""" def __init__(self, description="", level=None): diff --git a/lint-workflow/src/__init__.py b/lint-workflow/src/__init__.py new file mode 100644 index 00000000..df8c0a2d --- /dev/null +++ b/lint-workflow/src/__init__.py @@ -0,0 +1,2 @@ +from .models import * +from .load import * diff --git a/lint-workflow/src/load.py b/lint-workflow/src/load.py new file mode 100644 index 00000000..2619b869 --- /dev/null +++ b/lint-workflow/src/load.py @@ -0,0 +1,22 @@ +from ruamel.yaml import YAML + +from .models.job import Job +from .models.step import Step +from .models.workflow import Workflow + + +yaml = YAML() + + +def load_workflow(filename: str) -> Workflow: + with open(filename) as file: + #workflow = sanitize_yaml(file.read()) + workflow = yaml.load(file) + + return Workflow(**{ + **workflow, + "jobs": {str(job_key): Job(**{ + **job, + "steps": [Step(**step) for step in job["steps"]] + }) for job_key, job in workflow["jobs"].items()} + }) diff --git a/lint-workflow/src/models/__init__.py b/lint-workflow/src/models/__init__.py new file mode 100644 index 00000000..b564f68a --- /dev/null +++ b/lint-workflow/src/models/__init__.py @@ -0,0 +1,3 @@ +from .step import * +from .job import * +from .workflow import * diff --git a/lint-workflow/src/models/job.py b/lint-workflow/src/models/job.py new file mode 100644 index 00000000..3b083947 --- /dev/null +++ b/lint-workflow/src/models/job.py @@ -0,0 +1,19 @@ +from pydantic import BaseModel, Extra, Field, validator +from ruamel.yaml.comments import CommentedMap + +from src.models.step import Step + + +class Job(BaseModel, extra=Extra.allow): + name: str = None + runs_on: str = Field(..., alias="runs-on") + env: CommentedMap = None + needs: list[str] = None + steps: list[Step] = None + + #@validator('steps') + #def check_num_steps(cls, value): + # if len(value) < 1: + # raise ValueError("'jobs' require at least one 'step'") + # return value + diff --git a/lint-workflow/src/models/step.py b/lint-workflow/src/models/step.py new file mode 100644 index 00000000..57639f17 --- /dev/null +++ b/lint-workflow/src/models/step.py @@ -0,0 +1,23 @@ +from pydantic import BaseModel, Extra, Field, root_validator +from ruamel.yaml.comments import CommentedMap + + +class Step(BaseModel, extra=Extra.allow): + name: str = None + id: str = None + env: CommentedMap = None + uses: str = None + with_field: CommentedMap = Field(None, alias="with") + run: str = None + + #@root_validator + #def check_uses_or_run(cls, values): + # uses, run = values.get('uses'), values.get('run') + + # if uses is None and run is None: + # raise ValueError("Either 'uses' or 'run' must be set") + + # if uses is not None and run is not None: + # raise ValueError("cannot set both 'uses' and 'run'") + + # return values diff --git a/lint-workflow/src/models/workflow.py b/lint-workflow/src/models/workflow.py new file mode 100644 index 00000000..8b9e2875 --- /dev/null +++ b/lint-workflow/src/models/workflow.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel, Extra +from ruamel.yaml.comments import CommentedMap + +from src.models.job import Job + + +class Workflow(BaseModel, extra=Extra.allow): + name: str = None + on: CommentedMap = None + jobs: CommentedMap = None diff --git a/lint-workflow/src/rules.py b/lint-workflow/src/rules.py new file mode 100644 index 00000000..3a6a2a8b --- /dev/null +++ b/lint-workflow/src/rules.py @@ -0,0 +1,43 @@ +class LintFinding: + """Represents a linting problem.""" + + def __init__(self, description="", level=None): + self.description = description + self.level = level + + +class Rule: + def __init__(self, obj, field, rule, failure_message, failure_level="warning"): + self.obj = obj + self.field = field + self.rule = rule + self.failure_level = failure_level + self.failure_message = failure_message + + def run(self): + failure_message = f"{self.obj}.{self.field} => {self.failure_message}" + + if not self.rule(self.obj[self.field]): + return LintFinding(failure_message, self.failure_level) + return None + + +def enforce_field_exists(field): + return False if field is None else return True + +def enforce_field_starts_upper(field): + if field is None or not field[0].isupper(): + return False + return True + + +findings = [] + +rules: [ + Rule(workflow, "name", enforce_field_exists, "field required", "error") + Rule(workflow, "name", enforce_field_starts_upper, "field must be capitalized", "error") +] + + +for rule in rules: + findings.append(rule.run()) diff --git a/lint-workflow/tests/conftest.py b/lint-workflow/tests/conftest.py new file mode 100644 index 00000000..2315e5ef --- /dev/null +++ b/lint-workflow/tests/conftest.py @@ -0,0 +1 @@ +FIXTURE_DIR="./tests/fixtures" diff --git a/lint-workflow/tests/context.py b/lint-workflow/tests/context.py new file mode 100644 index 00000000..8fa51c8a --- /dev/null +++ b/lint-workflow/tests/context.py @@ -0,0 +1,5 @@ +import sys +import os +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +import src diff --git a/lint-workflow/tests/fixtures/test.yml b/lint-workflow/tests/fixtures/test.yml index 56ff2192..2fc40af7 100644 --- a/lint-workflow/tests/fixtures/test.yml +++ b/lint-workflow/tests/fixtures/test.yml @@ -53,4 +53,4 @@ jobs: localization_branch_name: crowdin-auto-sync create_pull_request: true pull_request_title: "Autosync Crowdin Translations" - pull_request_body: "Autosync the updated translations" \ No newline at end of file + pull_request_body: "Autosync the updated translations" diff --git a/lint-workflow/tests/test_action_update.py b/lint-workflow/tests/test_action_update.py index 8121e5ec..b2c12e6f 100644 --- a/lint-workflow/tests/test_action_update.py +++ b/lint-workflow/tests/test_action_update.py @@ -1,3 +1,4 @@ +import pytest import urllib3 as urllib from lint import get_action_update, memoized_action_update_urls @@ -5,6 +6,7 @@ http = urllib.PoolManager() +@pytest.mark.skip() def test_action_update(): action_id = "actions/checkout@86f86b36ef15e6570752e7175f451a512eac206b" sub_string = "github.com" diff --git a/lint-workflow/tests/test_job.py b/lint-workflow/tests/test_job.py new file mode 100644 index 00000000..d7d60e28 --- /dev/null +++ b/lint-workflow/tests/test_job.py @@ -0,0 +1,19 @@ +import json +import pytest + +from .context import src + + +@pytest.fixture +def job_default_data(): + return { + "name": "Test", + "runs-on": "ubuntu-latest", + "steps": [src.models.Step(run="echo stub")] + } + + +def test_job_creation(job_default_data): + job = src.models.Job(**job_default_data) + + assert job.name == "Test" diff --git a/lint-workflow/tests/test_lint.py b/lint-workflow/tests/test_lint.py index ecf84a3e..4f89a084 100644 --- a/lint-workflow/tests/test_lint.py +++ b/lint-workflow/tests/test_lint.py @@ -1,7 +1,10 @@ +import pytest + from lint import lint from .configs import FIXTURES_DIR +@pytest.mark.skip() def test_lint(capfd): file_path = f"{FIXTURES_DIR}/test.yml" lint_output = lint(file_path) diff --git a/lint-workflow/tests/test_load.py b/lint-workflow/tests/test_load.py new file mode 100644 index 00000000..f02a413d --- /dev/null +++ b/lint-workflow/tests/test_load.py @@ -0,0 +1,22 @@ +import json +import pytest + +from .conftest import FIXTURE_DIR +from .context import src + + +@pytest.fixture +def workflow_filename(): + return f"{FIXTURE_DIR}/test.yml" + + +def test_load_workflow(workflow_filename): + workflow = src.load.load_workflow(workflow_filename) + + assert workflow.name == "crowdin Pull" + assert len(workflow.jobs["crowdin-pull"].steps) == 4 + assert workflow.dict()["name"] == "crowdin Pull" + + print(workflow.dict(exclude_unset=True)) + + assert False diff --git a/lint-workflow/tests/test_main.py b/lint-workflow/tests/test_main.py index 36a83fda..bc4f863a 100644 --- a/lint-workflow/tests/test_main.py +++ b/lint-workflow/tests/test_main.py @@ -1,3 +1,5 @@ +import pytest + from lint import main from .configs import FIXTURES_DIR @@ -6,6 +8,7 @@ FIXTURES_DIR = "./tests/fixtures" +@pytest.mark.skip() def test_main_single_file(capsys): main([f"{FIXTURES_DIR}/test.yml"]) captured = capsys.readouterr() @@ -13,6 +16,7 @@ def test_main_single_file(capsys): assert "test.yml" in result +@pytest.mark.skip() def test_main_multiple_files(capsys): main([f"{FIXTURES_DIR}/test.yml {FIXTURES_DIR}/test-alt.yml"]) captured = capsys.readouterr() @@ -22,6 +26,7 @@ def test_main_multiple_files(capsys): assert "test-alt.yml" in result +@pytest.mark.skip() def test_main_folder(capsys): main([f"{FIXTURES_DIR}"]) captured = capsys.readouterr() @@ -31,6 +36,7 @@ def test_main_folder(capsys): assert "test-alt.yml" in result +@pytest.mark.skip() def test_main_folder_and_files(capsys): main([f"{FIXTURES_DIR}/test.yml {FIXTURES_DIR}"]) captured = capsys.readouterr() @@ -38,6 +44,7 @@ def test_main_folder_and_files(capsys): print(result) +@pytest.mark.skip() def test_main_not_found(capsys): # File that doesn't exist main(["not-a-real-file.yml"]) diff --git a/lint-workflow/tests/test_step.py b/lint-workflow/tests/test_step.py new file mode 100644 index 00000000..c942c86b --- /dev/null +++ b/lint-workflow/tests/test_step.py @@ -0,0 +1,44 @@ +import json +import pytest + +from .context import src + + +@pytest.fixture +def step_default_data(): + return { + "run": "echo \"test\"" + } + +def test_step_default(step_default_data): + step = src.models.Step(**step_default_data) + assert step.name == None + + +def test_step_keyword_field(step_default_data): + data = { + "with": {"config": "test.json"}, + **step_default_data + } + step = src.models.Step(**data) + assert "with_field" not in step.json(by_alias=True) + assert json.loads(step.json(by_alias=True))["with"] == {"config": "test.json"} + + +def test_step_no_keyword_field(step_default_data): + step = src.models.Step(**step_default_data) + assert step.with_field == None + assert "with_field" not in step.json(by_alias=True) + + +def test_step_kwargs(step_default_data): + data = { + "name": "test step", + "extra": "test", + **step_default_data + } + step = src.models.Step(**data) + + assert step.extra == "test" + + diff --git a/lint-workflow/tests/test_workflow.py b/lint-workflow/tests/test_workflow.py new file mode 100644 index 00000000..31196205 --- /dev/null +++ b/lint-workflow/tests/test_workflow.py @@ -0,0 +1,25 @@ +import json +import pytest + +from .context import src + + +@pytest.fixture +def workflow_default_data(): + return { + "name": "Test Workflow", + "on": {}, + "jobs": { + "job-key": src.models.Job(**{ + "name": "Test", + "runs-on": "ubuntu-latest", + "steps": [src.models.Step(run="echo stub")] + }) + } + } + + +def test_workflow_creation(workflow_default_data): + workflow = src.models.Workflow(**workflow_default_data) + + assert workflow.name == "Test Workflow" diff --git a/lint-workflow/tests/test_workflow_files.py b/lint-workflow/tests/test_workflow_files.py index 493fd6ca..6d969306 100644 --- a/lint-workflow/tests/test_workflow_files.py +++ b/lint-workflow/tests/test_workflow_files.py @@ -1,9 +1,11 @@ import os +import pytest from lint import workflow_files from .configs import FIXTURES_DIR +@pytest.mark.skip() def test_workflow_files(): assert workflow_files("") == [] assert workflow_files("not-a-real-file.yml") == [] From cdc06a1ebf207eb8d1a44e2486c35550599cb167 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 21 Apr 2023 06:28:04 -0700 Subject: [PATCH 02/57] migrating how the rules are created for a more streamlined experience --- lint-workflow/README.md | 35 +++ lint-workflow/cli.py | 422 +++++++++++++++++++++++++++++++ lint-workflow/lint.py | 10 +- lint-workflow/src/rules.py | 61 ++--- lint-workflow/tests/test_load.py | 4 +- 5 files changed, 491 insertions(+), 41 deletions(-) create mode 100644 lint-workflow/cli.py diff --git a/lint-workflow/README.md b/lint-workflow/README.md index 48cd5eed..d9474122 100644 --- a/lint-workflow/README.md +++ b/lint-workflow/README.md @@ -26,3 +26,38 @@ pytest tests pipenv shell black . ``` + + +## Design +### Objects + +**Workflow:** +**Jobs:** +**Steps (run || uses):** + + +### Rules + +```yaml +workflows: + - assert name exists + - assert name is capitalized +jobs: + - assert runner is pinned + - assert name exists + - assert name is capitalized + - assert any environment variables start with "_" +steps: + shared: + - assert name exists + - assert name is capitilized + uses: + - assert valid hash format - correct length + - assert valid hash format - cast to hexidecimal + - assert valid action repo path format + - assert action exists in GitHub + - warn out of date Action + run: + - assert correct format for single line run +``` + diff --git a/lint-workflow/cli.py b/lint-workflow/cli.py new file mode 100644 index 00000000..1d6e82ed --- /dev/null +++ b/lint-workflow/cli.py @@ -0,0 +1,422 @@ +import sys +import argparse +import os +import yaml +import json +import urllib3 as urllib +import logging + + +from src.rules import LintFinding + + +PROBLEM_LEVELS = { + "warning": 1, + "error": 2, +} + +memoized_action_update_urls = {} + + +class Colors: + """Class containing color codes for printing strings to output.""" + + black = "30m" + red = "31m" + green = "32m" + yellow = "33m" + blue = "34m" + magenta = "35m" + cyan = "36m" + white = "37m" + + +def get_max_error_level(findings): + """Get max error level from list of findings.""" + if len(findings) == 0: + return 0 + max_problem = max(findings, key=lambda finding: PROBLEM_LEVELS[finding.level]) + max_problem_level = PROBLEM_LEVELS[max_problem.level] + return max_problem_level + + +def print_finding(finding: LintFinding): + """Print formatted and colored finding.""" + if finding.level == "warning": + color = Colors.yellow + elif finding.level == "error": + color = Colors.red + else: + color = Colors.white + + line = f" - \033[{color}{finding.level}\033[0m {finding.description}" + + print(line) + + +def get_github_api_response(url, action_id): + """Call GitHub API with error logging without throwing an exception.""" + http = urllib.PoolManager() + headers = {"user-agent": "bw-linter"} + + if os.getenv("GITHUB_TOKEN", None): + headers["Authorization"] = f"Token {os.environ['GITHUB_TOKEN']}" + + response = http.request("GET", url, headers=headers) + + if response.status == 403 and response.reason == "rate limit exceeded": + logging.error( + f"Failed to call GitHub API for action: {action_id} due to rate limit exceeded." + ) + return None + + if response.status == 401 and response.reason == "Unauthorized": + logging.error( + f"Failed to call GitHub API for action: {action_id}: {response.data}." + ) + return None + + return response + + +def action_repo_exists(action_id): + """ + Takes and action id and checks if the action repo exists. + + Example action_id: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945 + """ + + if "./" in action_id: + # Handle local workflow calls, return None since there will be no updates. + return True + + path, *hash = action_id.split("@") + + if "bitwarden" in path: + path_list = path.split("/", 2) + url = f"https://api.github.com/repos/{path_list[0]}/{path_list[1]}" + response = get_github_api_response(url, action_id) + + else: + response = get_github_api_response( + f"https://api.github.com/repos/{path}", action_id + ) + + if response is None: + # Handle github api limit exceed by returning that the action exists without actually checking + # to prevent false errors on linter output. Only show it as an linter error. + return True + + if response.status == 404: + return False + + return True + + +def workflow_files(input: str) -> list: + """ + Takes in an argument of directory and/or files in string format from the CLI. + Returns a sorted set of all workflow files in the path(s) specified. + """ + workflow_files = [] + for path in input.split(): + if os.path.isfile(path): + workflow_files.append(path) + elif os.path.isdir(path): + for subdir, dirs, files in os.walk(path): + for filename in files: + filepath = subdir + os.sep + filename + if filepath.endswith((".yml", ".yaml")): + workflow_files.append(filepath) + + return sorted(set(workflow_files)) + + +def get_action_update(action_id): + """ + Takes in an action id (bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945) + and checks the action repo for the newest version. + If there is a new version, return the url to the updated version. + """ + if "./" in action_id: + # Handle local workflow calls, return None since there will be no updates. + return None + + path, *hash = action_id.split("@") + + if path in memoized_action_update_urls: + return memoized_action_update_urls[path] + else: + if "bitwarden" in path: + path_list = path.split("/", 2) + url = f"https://api.github.com/repos/{path_list[0]}/{path_list[1]}/commits?path={path_list[2]}" + response = get_github_api_response(url, action_id) + if not response: + return None + + sha = json.loads(response.data)[0]["sha"] + if sha not in hash: + update_url = ( + f"https://github.com/{path_list[0]}/{path_list[1]}/commit/{sha}" + ) + memoized_action_update_urls[path] = update_url + return update_url + else: + # Get tag from latest release + response = get_github_api_response( + f"https://api.github.com/repos/{path}/releases/latest", action_id + ) + if not response: + return None + + tag_name = json.loads(response.data)["tag_name"] + + # Get the URL to the commit for the tag + response = get_github_api_response( + f"https://api.github.com/repos/{path}/git/ref/tags/{tag_name}", + action_id, + ) + if not response: + return None + + if json.loads(response.data)["object"]["type"] == "commit": + sha = json.loads(response.data)["object"]["sha"] + else: + url = json.loads(response.data)["object"]["url"] + # Follow the URL and get the commit sha for tags + response = get_github_api_response(url, action_id) + if not response: + return None + + sha = json.loads(response.data)["object"]["sha"] + + if sha not in hash: + update_url = f"https://github.com/{path}/commit/{sha}" + memoized_action_update_urls[path] = update_url + return update_url + + +def lint(filename): + + findings = [] + max_error_level = 0 + + with open(filename) as file: + workflow = yaml.load(file, Loader=yaml.FullLoader) + + # Check for 'name' key for the workflow. + if "name" not in workflow: + findings.append(LintFinding("Name key missing for workflow.", "warning")) + + # Check for 'name' value to be capitalized in workflow. + elif not workflow["name"][0].isupper(): + findings.append( + LintFinding( + f"Name value for workflow is not capitalized. [{workflow['name']}]", + "warning", + ) + ) + + # Loop through jobs in workflow. + if "jobs" in workflow: + jobs = workflow["jobs"] + for job_key in jobs: + job = jobs[job_key] + + # Make sure runner is using pinned version. + runner = job.get("runs-on", "") + if "-latest" in runner: + findings.append( + LintFinding( + f"Runner version is set to '{runner}', but needs to be pinned to a version.", + "warning", + ) + ) + + # Check for 'name' key for job. + if "name" not in job: + findings.append( + LintFinding( + f"Name key missing for job key '{job_key}'.", "warning" + ) + ) + # Check for 'name' value to be capitalized in job. + elif not job["name"][0].isupper(): + findings.append( + LintFinding( + f"Name value of job key '{job_key}' is not capitalized. [{job['name']}]", + "warning", + ) + ) + + # If the job has environment variables defined, then make sure they start with an underscore. + if "env" in job: + for k in job["env"].keys(): + if k[0] != "_": + findings.append( + LintFinding( + f"Environment variable '{k}' of job key '{job_key}' does not start with an underscore.", + "warning", + ) + ) + + # Loop through steps in job. + steps = job.get("steps", "") + for i, step in enumerate(steps, start=1): + # Check for 'name' key for step. + if "name" not in step: + findings.append( + LintFinding( + f"Name key missing for step {str(i)} of job key '{job_key}'.", + "warning", + ) + ) + # Check for 'name' value to be capitalized in step. + elif not step["name"][0].isupper(): + findings.append( + LintFinding( + f"Name value in step {str(i)} of job key '{job_key}' is not capitalized. [{step['name']}]", + "warning", + ) + ) + + if "uses" in step: + try: + path, hash = step["uses"].split("@") + except ValueError: + logging.info("Skipping local action in workflow.") + break + + # If the step has a 'uses' key, check value hash. + try: + + # Check to make sure SHA1 hash is 40 characters. + if len(hash) != 40: + findings.append( + LintFinding( + f"Step {str(i)} of job key '{job_key}' does not have a valid action hash. (not 40 characters)", + "error", + ) + ) + + # Attempts to convert the hash to a integer + # which will succeed if all characters are hexadecimal + try: + int(hash, 16) + except ValueError: + findings.append( + LintFinding( + f"Step {str(i)} of job key '{job_key}' does not have a valid action hash. (not all hexadecimal characters)", + "error", + ) + ) + except: + findings.append( + LintFinding( + f"Step {str(i)} of job key '{job_key}' does not have a valid action hash. (missing '@' character)", + "error", + ) + ) + + # If the step has a 'uses' key, check path for external workflow + path_list = path.split("/", 2) + + if "bitwarden" in path and len(path_list) < 3: + findings.append( + LintFinding( + f"Step {str(i)} of job key '{job_key}' does not have a valid action path. (missing name of the repository or workflow)", + "error", + ) + ) + elif len(path_list) < 2: + findings.append( + LintFinding( + f"Step {str(i)} of job key '{job_key}' does not have a valid action path. (missing workflow name or the workflow author)", + "error", + ) + ) + # Check if GitHub repository with action exists + elif not action_repo_exists(step["uses"]): + action_id = step["uses"] + findings.append( + LintFinding( + f"Step {str(i)} of job key '{job_key}' uses an non-existing action: {action_id}.", + "error", + ) + ) + else: + # If the step has a 'uses' key and path is correct, check the action id repo for an update. + update_available = get_action_update(step["uses"]) + if update_available: + findings.append( + LintFinding( + f"Step {str(i)} of job key '{job_key}' uses an outdated action, consider updating it '{update_available}'.", + "warning", + ) + ) + + # If the step has a 'run' key and only has one command, check if it's a single line. + if "run" in step: + if step["run"].count("\n") == 1: + findings.append( + LintFinding( + f"Run in step {str(i)} of job key '{job_key}' should be a single line.", + "error", + ) + ) + + if len(findings) > 0: + print("#", filename) + for finding in findings: + print_finding(finding) + print() + + max_error_level = get_max_error_level(findings) + + return max_error_level + + +def main(input_args=None): + + # Pull the arguments from the command line + if not input_args: + input_args = sys.argv[1:] + + # Read arguments from command line. + parser = argparse.ArgumentParser() + parser.add_argument("input", help="file or directory input") + parser.add_argument( + "-s", + "--strict", + action="store_true", + help="return non-zero exit code on warnings " "as well as errors", + ) + args = parser.parse_args(input_args) + # max_error_level = 0 + + # for filename in input_files: + # prob_level = lint(filename) + # max_error_level = max(max_error_level, prob_level) + input_files = workflow_files(args.input) + if len(input_files) > 0: + prob_levels = list(map(lint, input_files)) + + max_error_level = max(prob_levels) + + if max_error_level == PROBLEM_LEVELS["error"]: + return_code = 2 + elif max_error_level == PROBLEM_LEVELS["warning"]: + return_code = 1 if args.strict else 0 + else: + return_code = 0 + + return return_code + else: + print(f'File(s)/Directory: "{args.input}" does not exist, exiting.') + return -1 + + +if __name__ == "__main__": + return_code = main() + print(memoized_action_update_urls) + sys.exit(return_code) diff --git a/lint-workflow/lint.py b/lint-workflow/lint.py index 7e121fed..94e78678 100644 --- a/lint-workflow/lint.py +++ b/lint-workflow/lint.py @@ -6,12 +6,12 @@ import urllib3 as urllib import logging + PROBLEM_LEVELS = { "warning": 1, "error": 2, } - memoized_action_update_urls = {} @@ -28,14 +28,6 @@ class Colors: white = "37m" -class LintFinding: - """Represents a linting problem.""" - - def __init__(self, description="", level=None): - self.description = description - self.level = level - - def get_max_error_level(findings): """Get max error level from list of findings.""" if len(findings) == 0: diff --git a/lint-workflow/src/rules.py b/lint-workflow/src/rules.py index 3a6a2a8b..2e990f22 100644 --- a/lint-workflow/src/rules.py +++ b/lint-workflow/src/rules.py @@ -1,43 +1,44 @@ -class LintFinding: - """Represents a linting problem.""" +from collections.abc import Callable - def __init__(self, description="", level=None): - self.description = description - self.level = level +from .models.workflow import Workflow +from .models.job import Job +from .models.step import Step -class Rule: - def __init__(self, obj, field, rule, failure_message, failure_level="warning"): - self.obj = obj - self.field = field - self.rule = rule - self.failure_level = failure_level - self.failure_message = failure_message +class LintFinding: + """Represents a linting problem.""" + description: str = "" + level: str = None - def run(self): - failure_message = f"{self.obj}.{self.field} => {self.failure_message}" - if not self.rule(self.obj[self.field]): - return LintFinding(failure_message, self.failure_level) - return None +def _validate( + obj: Workflow | Job | Step, + rule: Callable[[Workflow | Job | Step], bool | None], + message: str, + warning_level: str +) -> LintFinding | None: + try: + if rule(obj): + return None + except: + message = f"failed to apply {rule.__name__}" + warning_level = "error" + return LintFinding(f"{obj.__name__}.{obj.name} => {message}", warning_level) -def enforce_field_exists(field): - return False if field is None else return True -def enforce_field_starts_upper(field): - if field is None or not field[0].isupper(): - return False - return True +# -------- Rules --------- +def workflow_name_exists( obj: Workflow | Job | Step): + return obj.name is not None -findings = [] +def workflow_name_capitalized( obj: Workflow | Job | Step): + return obj.name.isupper() -rules: [ - Rule(workflow, "name", enforce_field_exists, "field required", "error") - Rule(workflow, "name", enforce_field_starts_upper, "field must be capitalized", "error") -] +# ----- End of Rules ----- -for rule in rules: - findings.append(rule.run()) +findings = list(filter(lambda a: a is not None, [ + _validate(workflow, workflow_name_exists, "field required", "error"), + _validate(workflow, workflow_name_capitalized, "field must be capitalized", "error") +])) diff --git a/lint-workflow/tests/test_load.py b/lint-workflow/tests/test_load.py index f02a413d..2374b931 100644 --- a/lint-workflow/tests/test_load.py +++ b/lint-workflow/tests/test_load.py @@ -17,6 +17,6 @@ def test_load_workflow(workflow_filename): assert len(workflow.jobs["crowdin-pull"].steps) == 4 assert workflow.dict()["name"] == "crowdin Pull" - print(workflow.dict(exclude_unset=True)) + #print(workflow.dict(exclude_unset=True)) - assert False + #assert False From 704a5ed7db6f90f4c43b3f7a4e4991a33fc13d26 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 21 Apr 2023 10:30:38 -0700 Subject: [PATCH 03/57] removing the keyword from the step model since there are no linting tests run on it or any of it's children --- lint-workflow/Pipfile.lock | 182 ++++++++++++++++--------------- lint-workflow/src/models/step.py | 9 +- 2 files changed, 100 insertions(+), 91 deletions(-) diff --git a/lint-workflow/Pipfile.lock b/lint-workflow/Pipfile.lock index 54e9614b..9ae96a01 100644 --- a/lint-workflow/Pipfile.lock +++ b/lint-workflow/Pipfile.lock @@ -18,45 +18,45 @@ "default": { "pydantic": { "hashes": [ - "sha256:05a81b006be15655b2a1bae5faa4280cf7c81d0e09fcb49b342ebf826abe5a72", - "sha256:0b53e1d41e97063d51a02821b80538053ee4608b9a181c1005441f1673c55423", - "sha256:2b3ce5f16deb45c472dde1a0ee05619298c864a20cded09c4edd820e1454129f", - "sha256:2e82a6d37a95e0b1b42b82ab340ada3963aea1317fd7f888bb6b9dfbf4fff57c", - "sha256:301d626a59edbe5dfb48fcae245896379a450d04baeed50ef40d8199f2733b06", - "sha256:39f4a73e5342b25c2959529f07f026ef58147249f9b7431e1ba8414a36761f53", - "sha256:4948f264678c703f3877d1c8877c4e3b2e12e549c57795107f08cf70c6ec7774", - "sha256:4b05697738e7d2040696b0a66d9f0a10bec0efa1883ca75ee9e55baf511909d6", - "sha256:51bdeb10d2db0f288e71d49c9cefa609bca271720ecd0c58009bd7504a0c464c", - "sha256:55b1625899acd33229c4352ce0ae54038529b412bd51c4915349b49ca575258f", - "sha256:572066051eeac73d23f95ba9a71349c42a3e05999d0ee1572b7860235b850cc6", - "sha256:6a05a9db1ef5be0fe63e988f9617ca2551013f55000289c671f71ec16f4985e3", - "sha256:6dc1cc241440ed7ca9ab59d9929075445da6b7c94ced281b3dd4cfe6c8cff817", - "sha256:6e7124d6855b2780611d9f5e1e145e86667eaa3bd9459192c8dc1a097f5e9903", - "sha256:75d52162fe6b2b55964fbb0af2ee58e99791a3138588c482572bb6087953113a", - "sha256:78cec42b95dbb500a1f7120bdf95c401f6abb616bbe8785ef09887306792e66e", - "sha256:7feb6a2d401f4d6863050f58325b8d99c1e56f4512d98b11ac64ad1751dc647d", - "sha256:8775d4ef5e7299a2f4699501077a0defdaac5b6c4321173bcb0f3c496fbadf85", - "sha256:887ca463c3bc47103c123bc06919c86720e80e1214aab79e9b779cda0ff92a00", - "sha256:9193d4f4ee8feca58bc56c8306bcb820f5c7905fd919e0750acdeeeef0615b28", - "sha256:983e720704431a6573d626b00662eb78a07148c9115129f9b4351091ec95ecc3", - "sha256:990406d226dea0e8f25f643b370224771878142155b879784ce89f633541a024", - "sha256:9cbdc268a62d9a98c56e2452d6c41c0263d64a2009aac69246486f01b4f594c4", - "sha256:a48f1953c4a1d9bd0b5167ac50da9a79f6072c63c4cef4cf2a3736994903583e", - "sha256:a9a6747cac06c2beb466064dda999a13176b23535e4c496c9d48e6406f92d42d", - "sha256:a9f2de23bec87ff306aef658384b02aa7c32389766af3c5dee9ce33e80222dfa", - "sha256:b5635de53e6686fe7a44b5cf25fcc419a0d5e5c1a1efe73d49d48fe7586db854", - "sha256:b6f9d649892a6f54a39ed56b8dfd5e08b5f3be5f893da430bed76975f3735d15", - "sha256:b9a3859f24eb4e097502a3be1fb4b2abb79b6103dd9e2e0edb70613a4459a648", - "sha256:cd8702c5142afda03dc2b1ee6bc358b62b3735b2cce53fc77b31ca9f728e4bc8", - "sha256:d7b5a3821225f5c43496c324b0d6875fde910a1c2933d726a743ce328fbb2a8c", - "sha256:d88c4c0e5c5dfd05092a4b271282ef0588e5f4aaf345778056fc5259ba098857", - "sha256:eb992a1ef739cc7b543576337bebfc62c0e6567434e522e97291b251a41dad7f", - "sha256:f2f7eb6273dd12472d7f218e1fef6f7c7c2f00ac2e1ecde4db8824c457300416", - "sha256:fdf88ab63c3ee282c76d652fc86518aacb737ff35796023fae56a65ced1a5978", - "sha256:fdf8d759ef326962b4678d89e275ffc55b7ce59d917d9f72233762061fd04a2d" + "sha256:01aea3a42c13f2602b7ecbbea484a98169fb568ebd9e247593ea05f01b884b2e", + "sha256:0cd181f1d0b1d00e2b705f1bf1ac7799a2d938cce3376b8007df62b29be3c2c6", + "sha256:10a86d8c8db68086f1e30a530f7d5f83eb0685e632e411dbbcf2d5c0150e8dcd", + "sha256:193924c563fae6ddcb71d3f06fa153866423ac1b793a47936656e806b64e24ca", + "sha256:464855a7ff7f2cc2cf537ecc421291b9132aa9c79aef44e917ad711b4a93163b", + "sha256:516f1ed9bc2406a0467dd777afc636c7091d71f214d5e413d64fef45174cfc7a", + "sha256:6434b49c0b03a51021ade5c4daa7d70c98f7a79e95b551201fff682fc1661245", + "sha256:64d34ab766fa056df49013bb6e79921a0265204c071984e75a09cbceacbbdd5d", + "sha256:670bb4683ad1e48b0ecb06f0cfe2178dcf74ff27921cdf1606e527d2617a81ee", + "sha256:68792151e174a4aa9e9fc1b4e653e65a354a2fa0fed169f7b3d09902ad2cb6f1", + "sha256:701daea9ffe9d26f97b52f1d157e0d4121644f0fcf80b443248434958fd03dc3", + "sha256:7d45fc99d64af9aaf7e308054a0067fdcd87ffe974f2442312372dfa66e1001d", + "sha256:80b1fab4deb08a8292d15e43a6edccdffa5377a36a4597bb545b93e79c5ff0a5", + "sha256:82dffb306dd20bd5268fd6379bc4bfe75242a9c2b79fec58e1041fbbdb1f7914", + "sha256:8c7f51861d73e8b9ddcb9916ae7ac39fb52761d9ea0df41128e81e2ba42886cd", + "sha256:950ce33857841f9a337ce07ddf46bc84e1c4946d2a3bba18f8280297157a3fd1", + "sha256:976cae77ba6a49d80f461fd8bba183ff7ba79f44aa5cfa82f1346b5626542f8e", + "sha256:9f6f0fd68d73257ad6685419478c5aece46432f4bdd8d32c7345f1986496171e", + "sha256:a7cd2251439988b413cb0a985c4ed82b6c6aac382dbaff53ae03c4b23a70e80a", + "sha256:abfb7d4a7cd5cc4e1d1887c43503a7c5dd608eadf8bc615413fc498d3e4645cd", + "sha256:ae150a63564929c675d7f2303008d88426a0add46efd76c3fc797cd71cb1b46f", + "sha256:b0f85904f73161817b80781cc150f8b906d521fa11e3cdabae19a581c3606209", + "sha256:b4a849d10f211389502059c33332e91327bc154acc1845f375a99eca3afa802d", + "sha256:c15582f9055fbc1bfe50266a19771bbbef33dd28c45e78afbe1996fd70966c2a", + "sha256:c230c0d8a322276d6e7b88c3f7ce885f9ed16e0910354510e0bae84d54991143", + "sha256:cc1dde4e50a5fc1336ee0581c1612215bc64ed6d28d2c7c6f25d2fe3e7c3e918", + "sha256:cf135c46099ff3f919d2150a948ce94b9ce545598ef2c6c7bf55dca98a304b52", + "sha256:cfc83c0678b6ba51b0532bea66860617c4cd4251ecf76e9846fa5a9f3454e97e", + "sha256:d2a5ebb48958754d386195fe9e9c5106f11275867051bf017a8059410e9abf1f", + "sha256:d71e69699498b020ea198468e2480a2f1e7433e32a3a99760058c6520e2bea7e", + "sha256:d75ae19d2a3dbb146b6f324031c24f8a3f52ff5d6a9f22f0683694b3afcb16fb", + "sha256:dfe2507b8ef209da71b6fb5f4e597b50c5a34b78d7e857c4f8f3115effaef5fe", + "sha256:e0cfe895a504c060e5d36b287ee696e2fdad02d89e0d895f83037245218a87fe", + "sha256:e79e999e539872e903767c417c897e729e015872040e56b96e67968c3b918b2d", + "sha256:ecbbc51391248116c0a055899e6c3e7ffbb11fb5e2a4cd6f2d0b93272118a209", + "sha256:f4a2b50e2b03d5776e7f21af73e2070e1b5c0d0df255a827e7c632962f8315af" ], "index": "pypi", - "version": "==1.10.4" + "version": "==1.10.7" }, "pyyaml": { "hashes": [ @@ -142,59 +142,66 @@ "sha256:d000f258cf42fec2b1bbf2863c61d7b8918d31ffee905da62dede869254d3b8a", "sha256:d5859983f26d8cd7bb5c287ef452e8aacc86501487634573d260968f753e1d71", "sha256:d5e51e2901ec2366b79f16c2299a03e74ba4531ddcfacc1416639c557aef0ad8", + "sha256:da538167284de58a52109a9b89b8f6a53ff8437dd6dc26d33b57bf6699153122", "sha256:debc87a9516b237d0466a711b18b6ebeb17ba9f391eb7f91c649c5c4ec5006c7", "sha256:df5828871e6648db72d1c19b4bd24819b80a755c4541d3409f0f7acd0f335c80", "sha256:ecdf1a604009bd35c674b9225a8fa609e0282d9b896c03dd441a91e5f53b534e", "sha256:efa08d63ef03d079dcae1dfe334f6c8847ba8b645d08df286358b1f5293d24ab", "sha256:f01da5790e95815eb5a8a138508c01c758e5f5bc0ce4286c4f7028b8dd7ac3d0", - "sha256:f34019dced51047d6f70cb9383b2ae2853b7fc4dce65129a5acd49f4f9256646" + "sha256:f34019dced51047d6f70cb9383b2ae2853b7fc4dce65129a5acd49f4f9256646", + "sha256:f6d3d39611ac2e4f62c3128a9eed45f19a6608670c5a2f4f07f24e8de3441d38" ], "markers": "python_version < '3.11' and platform_python_implementation == 'CPython'", "version": "==0.2.7" }, "typing-extensions": { "hashes": [ - "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", - "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" + "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb", + "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4" ], "markers": "python_version >= '3.7'", - "version": "==4.4.0" + "version": "==4.5.0" }, "urllib3": { "hashes": [ - "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72", - "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1" + "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305", + "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42" ], "index": "pypi", - "version": "==1.26.14" + "version": "==1.26.15" } }, "develop": { - "attrs": { - "hashes": [ - "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836", - "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99" - ], - "markers": "python_version >= '3.6'", - "version": "==22.2.0" - }, "black": { "hashes": [ - "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320", - "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351", - "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350", - "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f", - "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf", - "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148", - "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4", - "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d", - "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc", - "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d", - "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2", - "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f" + "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5", + "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915", + "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326", + "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940", + "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b", + "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30", + "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c", + "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c", + "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab", + "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27", + "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2", + "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961", + "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9", + "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb", + "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70", + "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331", + "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2", + "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266", + "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d", + "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6", + "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b", + "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925", + "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8", + "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4", + "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3" ], "index": "pypi", - "version": "==22.12.0" + "version": "==23.3.0" }, "click": { "hashes": [ @@ -206,11 +213,11 @@ }, "exceptiongroup": { "hashes": [ - "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e", - "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23" + "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e", + "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785" ], "markers": "python_version < '3.11'", - "version": "==1.1.0" + "version": "==1.1.1" }, "iniconfig": { "hashes": [ @@ -222,34 +229,35 @@ }, "mypy-extensions": { "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" ], - "version": "==0.4.3" + "markers": "python_version >= '3.5'", + "version": "==1.0.0" }, "packaging": { "hashes": [ - "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2", - "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97" + "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", + "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f" ], "markers": "python_version >= '3.7'", - "version": "==23.0" + "version": "==23.1" }, "pathspec": { "hashes": [ - "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6", - "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6" + "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687", + "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293" ], "markers": "python_version >= '3.7'", - "version": "==0.10.3" + "version": "==0.11.1" }, "platformdirs": { "hashes": [ - "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490", - "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2" + "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08", + "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e" ], "markers": "python_version >= '3.7'", - "version": "==2.6.2" + "version": "==3.2.0" }, "pluggy": { "hashes": [ @@ -261,27 +269,27 @@ }, "pytest": { "hashes": [ - "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71", - "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59" + "sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362", + "sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3" ], "index": "pypi", - "version": "==7.2.0" + "version": "==7.3.1" }, "tomli": { "hashes": [ "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" ], - "markers": "python_full_version < '3.11.0a7'", + "markers": "python_version < '3.11'", "version": "==2.0.1" }, "typing-extensions": { "hashes": [ - "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", - "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" + "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb", + "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4" ], "markers": "python_version >= '3.7'", - "version": "==4.4.0" + "version": "==4.5.0" } } } diff --git a/lint-workflow/src/models/step.py b/lint-workflow/src/models/step.py index 57639f17..17b66226 100644 --- a/lint-workflow/src/models/step.py +++ b/lint-workflow/src/models/step.py @@ -1,13 +1,14 @@ -from pydantic import BaseModel, Extra, Field, root_validator +from dataclasses import dataclass, field + +#from pydantic import BaseModel, Extra, Field, root_validator from ruamel.yaml.comments import CommentedMap -class Step(BaseModel, extra=Extra.allow): +@dataclass +class Step: name: str = None - id: str = None env: CommentedMap = None uses: str = None - with_field: CommentedMap = Field(None, alias="with") run: str = None #@root_validator From 4b36ac8416e3de65e887c7522b51e2c6f882b124 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 5 May 2023 12:37:23 -0700 Subject: [PATCH 04/57] Switch from pydantic to dataclasses --- lint-workflow/Pipfile | 1 + lint-workflow/Pipfile.lock | 68 ++++++++++++++++++---- lint-workflow/README.md | 2 +- lint-workflow/lint.py | 6 ++ lint-workflow/src/models/job.py | 24 ++++---- lint-workflow/src/models/step.py | 24 +++----- lint-workflow/src/models/workflow.py | 8 ++- lint-workflow/tests/fixtures/test-min.yaml | 12 ++++ lint-workflow/tests/test_job.py | 7 ++- lint-workflow/tests/test_step.py | 7 ++- lint-workflow/tests/test_workflow.py | 9 ++- 11 files changed, 117 insertions(+), 51 deletions(-) create mode 100644 lint-workflow/tests/fixtures/test-min.yaml diff --git a/lint-workflow/Pipfile b/lint-workflow/Pipfile index 968707a7..88b0b387 100644 --- a/lint-workflow/Pipfile +++ b/lint-workflow/Pipfile @@ -8,6 +8,7 @@ pyyaml = "*" urllib3 = "*" pydantic = "*" "ruamel.yaml" = "*" +dataclasses-json = "*" [dev-packages] black = "*" diff --git a/lint-workflow/Pipfile.lock b/lint-workflow/Pipfile.lock index 9ae96a01..052ac1c7 100644 --- a/lint-workflow/Pipfile.lock +++ b/lint-workflow/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "0316d5bb4d819d8f0e3ef9b8c91dbbef236167a3d44b5d94f42ef70b2172a8aa" + "sha256": "a1b4e992db55833a7293809f75176ba5ebc8c68aa29f4ed3b51c6a621cf909b3" }, "pipfile-spec": 6, "requires": { @@ -16,6 +16,45 @@ ] }, "default": { + "dataclasses-json": { + "hashes": [ + "sha256:bc285b5f892094c3a53d558858a88553dd6a61a11ab1a8128a0e554385dcc5dd", + "sha256:c2c11bc8214fbf709ffc369d11446ff6945254a7f09128154a7620613d8fda90" + ], + "index": "pypi", + "version": "==0.5.7" + }, + "marshmallow": { + "hashes": [ + "sha256:90032c0fd650ce94b6ec6dc8dfeb0e3ff50c144586462c389b81a07205bedb78", + "sha256:93f0958568da045b0021ec6aeb7ac37c81bfcccbb9a0e7ed8559885070b3a19b" + ], + "markers": "python_version >= '3.7'", + "version": "==3.19.0" + }, + "marshmallow-enum": { + "hashes": [ + "sha256:38e697e11f45a8e64b4a1e664000897c659b60aa57bfa18d44e226a9920b6e58", + "sha256:57161ab3dbfde4f57adeb12090f39592e992b9c86d206d02f6bd03ebec60f072" + ], + "version": "==1.5.1" + }, + "mypy-extensions": { + "hashes": [ + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.0" + }, + "packaging": { + "hashes": [ + "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", + "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f" + ], + "markers": "python_version >= '3.7'", + "version": "==23.1" + }, "pydantic": { "hashes": [ "sha256:01aea3a42c13f2602b7ecbbea484a98169fb568ebd9e247593ea05f01b884b2e", @@ -106,11 +145,11 @@ }, "ruamel.yaml": { "hashes": [ - "sha256:742b35d3d665023981bd6d16b3d24248ce5df75fdb4e2924e93a05c1f8b61ca7", - "sha256:8b7ce697a2f212752a35c1ac414471dc16c424c9573be4926b56ff3f5d23b7af" + "sha256:40e549c264e799d8e4cd9e54e869ffa60c9436e1ef497500b08fbda3b92a1517", + "sha256:605510839d5af3ae730f3ed6408b7004c83f97aa2b732f6f366da7c2fc2f0d9c" ], "index": "pypi", - "version": "==0.17.21" + "version": "==0.17.23" }, "ruamel.yaml.clib": { "hashes": [ @@ -151,7 +190,7 @@ "sha256:f34019dced51047d6f70cb9383b2ae2853b7fc4dce65129a5acd49f4f9256646", "sha256:f6d3d39611ac2e4f62c3128a9eed45f19a6608670c5a2f4f07f24e8de3441d38" ], - "markers": "python_version < '3.11' and platform_python_implementation == 'CPython'", + "markers": "python_version < '3.12' and platform_python_implementation == 'CPython'", "version": "==0.2.7" }, "typing-extensions": { @@ -162,13 +201,20 @@ "markers": "python_version >= '3.7'", "version": "==4.5.0" }, + "typing-inspect": { + "hashes": [ + "sha256:5fbf9c1e65d4fa01e701fe12a5bca6c6e08a4ffd5bc60bfac028253a447c5188", + "sha256:8b1ff0c400943b6145df8119c41c244ca8207f1f10c9c057aeed1560e4806e3d" + ], + "version": "==0.8.0" + }, "urllib3": { "hashes": [ - "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305", - "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42" + "sha256:61717a1095d7e155cdb737ac7bb2f4324a858a1e2e6466f6d03ff630ca68d3cc", + "sha256:d055c2f9d38dc53c808f6fdc8eab7360b6fdbbde02340ed25cfbcd817c62469e" ], "index": "pypi", - "version": "==1.26.15" + "version": "==2.0.2" } }, "develop": { @@ -253,11 +299,11 @@ }, "platformdirs": { "hashes": [ - "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08", - "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e" + "sha256:47692bc24c1958e8b0f13dd727307cff1db103fca36399f457da8e05f222fdc4", + "sha256:7954a68d0ba23558d753f73437c55f89027cf8f5108c19844d4b82e5af396335" ], "markers": "python_version >= '3.7'", - "version": "==3.2.0" + "version": "==3.5.0" }, "pluggy": { "hashes": [ diff --git a/lint-workflow/README.md b/lint-workflow/README.md index d9474122..7abf293a 100644 --- a/lint-workflow/README.md +++ b/lint-workflow/README.md @@ -43,7 +43,7 @@ workflows: - assert name exists - assert name is capitalized jobs: - - assert runner is pinned + - assert runs-on is pinned - assert name exists - assert name is capitalized - assert any environment variables start with "_" diff --git a/lint-workflow/lint.py b/lint-workflow/lint.py index 94e78678..7afa0769 100644 --- a/lint-workflow/lint.py +++ b/lint-workflow/lint.py @@ -28,6 +28,12 @@ class Colors: white = "37m" +class LintFinding: + """Represents a linting problem.""" + description: str = "" + level: str = None + + def get_max_error_level(findings): """Get max error level from list of findings.""" if len(findings) == 0: diff --git a/lint-workflow/src/models/job.py b/lint-workflow/src/models/job.py index 3b083947..acfa9234 100644 --- a/lint-workflow/src/models/job.py +++ b/lint-workflow/src/models/job.py @@ -1,19 +1,15 @@ -from pydantic import BaseModel, Extra, Field, validator +from dataclasses import dataclass, field +from typing import List, Optional + +from dataclasses_json import config, dataclass_json from ruamel.yaml.comments import CommentedMap from src.models.step import Step - -class Job(BaseModel, extra=Extra.allow): +@dataclass_json +@dataclass +class Job: + runs_on: str = field(metadata=config(field_name="runs-on")) name: str = None - runs_on: str = Field(..., alias="runs-on") - env: CommentedMap = None - needs: list[str] = None - steps: list[Step] = None - - #@validator('steps') - #def check_num_steps(cls, value): - # if len(value) < 1: - # raise ValueError("'jobs' require at least one 'step'") - # return value - + env: Optional[CommentedMap] = None + steps: List[Step] = None diff --git a/lint-workflow/src/models/step.py b/lint-workflow/src/models/step.py index 17b66226..3ec72800 100644 --- a/lint-workflow/src/models/step.py +++ b/lint-workflow/src/models/step.py @@ -1,24 +1,14 @@ from dataclasses import dataclass, field +from typing import Optional -#from pydantic import BaseModel, Extra, Field, root_validator +from dataclasses_json import dataclass_json from ruamel.yaml.comments import CommentedMap +@dataclass_json @dataclass class Step: - name: str = None - env: CommentedMap = None - uses: str = None - run: str = None - - #@root_validator - #def check_uses_or_run(cls, values): - # uses, run = values.get('uses'), values.get('run') - - # if uses is None and run is None: - # raise ValueError("Either 'uses' or 'run' must be set") - - # if uses is not None and run is not None: - # raise ValueError("cannot set both 'uses' and 'run'") - - # return values + name: Optional[str] = None + env: Optional[CommentedMap] = None + uses: Optional[str] = None + run: Optional[str] = None diff --git a/lint-workflow/src/models/workflow.py b/lint-workflow/src/models/workflow.py index 8b9e2875..146dadbf 100644 --- a/lint-workflow/src/models/workflow.py +++ b/lint-workflow/src/models/workflow.py @@ -1,10 +1,14 @@ -from pydantic import BaseModel, Extra +from dataclasses import dataclass, field + +from dataclasses_json import config, dataclass_json from ruamel.yaml.comments import CommentedMap from src.models.job import Job -class Workflow(BaseModel, extra=Extra.allow): +@dataclass_json +@dataclass +class Workflow: name: str = None on: CommentedMap = None jobs: CommentedMap = None diff --git a/lint-workflow/tests/fixtures/test-min.yaml b/lint-workflow/tests/fixtures/test-min.yaml new file mode 100644 index 00000000..958883f4 --- /dev/null +++ b/lint-workflow/tests/fixtures/test-min.yaml @@ -0,0 +1,12 @@ +--- +name: Test Workflow + +on: + workflow_dispatch: + +jobs: + job-key: + name: Test + runs-on: ubuntu-latest + steps: + - run: echo test diff --git a/lint-workflow/tests/test_job.py b/lint-workflow/tests/test_job.py index d7d60e28..36873e3a 100644 --- a/lint-workflow/tests/test_job.py +++ b/lint-workflow/tests/test_job.py @@ -13,7 +13,10 @@ def job_default_data(): } -def test_job_creation(job_default_data): - job = src.models.Job(**job_default_data) +def test_job_default(job_default_data): + job = src.models.Job.from_dict(job_default_data) assert job.name == "Test" + assert job.runs_on == "ubuntu-latest" + assert job.env == None + assert len(job.steps) == 1 diff --git a/lint-workflow/tests/test_step.py b/lint-workflow/tests/test_step.py index c942c86b..426c6d07 100644 --- a/lint-workflow/tests/test_step.py +++ b/lint-workflow/tests/test_step.py @@ -11,10 +11,14 @@ def step_default_data(): } def test_step_default(step_default_data): - step = src.models.Step(**step_default_data) + step = src.models.Step.from_dict(step_default_data) assert step.name == None + assert step.env == None + assert step.uses == None + assert step.run == "echo \"test\"" +@pytest.mark.skip() def test_step_keyword_field(step_default_data): data = { "with": {"config": "test.json"}, @@ -25,6 +29,7 @@ def test_step_keyword_field(step_default_data): assert json.loads(step.json(by_alias=True))["with"] == {"config": "test.json"} +@pytest.mark.skip() def test_step_no_keyword_field(step_default_data): step = src.models.Step(**step_default_data) assert step.with_field == None diff --git a/lint-workflow/tests/test_workflow.py b/lint-workflow/tests/test_workflow.py index 31196205..b296219e 100644 --- a/lint-workflow/tests/test_workflow.py +++ b/lint-workflow/tests/test_workflow.py @@ -1,6 +1,7 @@ import json import pytest +from .conftest import FIXTURE_DIR from .context import src @@ -10,16 +11,18 @@ def workflow_default_data(): "name": "Test Workflow", "on": {}, "jobs": { - "job-key": src.models.Job(**{ + "job-key": src.models.Job.from_dict({ "name": "Test", "runs-on": "ubuntu-latest", - "steps": [src.models.Step(run="echo stub")] + "steps": [src.models.Step.from_dict({ "run": "echo stub"})] }) } } -def test_workflow_creation(workflow_default_data): +def test_workflow_default(workflow_default_data): workflow = src.models.Workflow(**workflow_default_data) assert workflow.name == "Test Workflow" + assert len(workflow.on.keys()) == 0 + assert len(workflow.jobs.keys()) == 1 From 1ef4a3bd5aca5e05da977368bb9e7ee207a22b2c Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 5 May 2023 12:53:34 -0700 Subject: [PATCH 05/57] Update to use the correct loader (Model.from_dict({})). Add extra data tests --- lint-workflow/src/load.py | 6 +++--- lint-workflow/src/models/job.py | 4 ++-- lint-workflow/src/models/step.py | 4 ++-- lint-workflow/src/models/workflow.py | 4 ++-- lint-workflow/tests/test_job.py | 10 ++++++++++ lint-workflow/tests/test_load.py | 5 ----- lint-workflow/tests/test_step.py | 16 +++++++--------- lint-workflow/tests/test_workflow.py | 10 ++++++++++ 8 files changed, 36 insertions(+), 23 deletions(-) diff --git a/lint-workflow/src/load.py b/lint-workflow/src/load.py index 2619b869..e5530c9c 100644 --- a/lint-workflow/src/load.py +++ b/lint-workflow/src/load.py @@ -13,10 +13,10 @@ def load_workflow(filename: str) -> Workflow: #workflow = sanitize_yaml(file.read()) workflow = yaml.load(file) - return Workflow(**{ + return Workflow.from_dict({ **workflow, - "jobs": {str(job_key): Job(**{ + "jobs": {str(job_key): Job.from_dict({ **job, - "steps": [Step(**step) for step in job["steps"]] + "steps": [Step.from_dict(step) for step in job["steps"]] }) for job_key, job in workflow["jobs"].items()} }) diff --git a/lint-workflow/src/models/job.py b/lint-workflow/src/models/job.py index acfa9234..b8d12622 100644 --- a/lint-workflow/src/models/job.py +++ b/lint-workflow/src/models/job.py @@ -1,12 +1,12 @@ from dataclasses import dataclass, field from typing import List, Optional -from dataclasses_json import config, dataclass_json +from dataclasses_json import config, dataclass_json, Undefined from ruamel.yaml.comments import CommentedMap from src.models.step import Step -@dataclass_json +@dataclass_json(undefined=Undefined.EXCLUDE) @dataclass class Job: runs_on: str = field(metadata=config(field_name="runs-on")) diff --git a/lint-workflow/src/models/step.py b/lint-workflow/src/models/step.py index 3ec72800..6ec5f94c 100644 --- a/lint-workflow/src/models/step.py +++ b/lint-workflow/src/models/step.py @@ -1,11 +1,11 @@ from dataclasses import dataclass, field from typing import Optional -from dataclasses_json import dataclass_json +from dataclasses_json import dataclass_json, Undefined from ruamel.yaml.comments import CommentedMap -@dataclass_json +@dataclass_json(undefined=Undefined.EXCLUDE) @dataclass class Step: name: Optional[str] = None diff --git a/lint-workflow/src/models/workflow.py b/lint-workflow/src/models/workflow.py index 146dadbf..50beb154 100644 --- a/lint-workflow/src/models/workflow.py +++ b/lint-workflow/src/models/workflow.py @@ -1,12 +1,12 @@ from dataclasses import dataclass, field -from dataclasses_json import config, dataclass_json +from dataclasses_json import config, dataclass_json, Undefined from ruamel.yaml.comments import CommentedMap from src.models.job import Job -@dataclass_json +@dataclass_json(undefined=Undefined.EXCLUDE) @dataclass class Workflow: name: str = None diff --git a/lint-workflow/tests/test_job.py b/lint-workflow/tests/test_job.py index 36873e3a..e505bb94 100644 --- a/lint-workflow/tests/test_job.py +++ b/lint-workflow/tests/test_job.py @@ -20,3 +20,13 @@ def test_job_default(job_default_data): assert job.runs_on == "ubuntu-latest" assert job.env == None assert len(job.steps) == 1 + + +def test_job_extra_kwargs(job_default_data): + job = src.models.Job.from_dict({ + "extra": "test", + **job_default_data + }) + + with pytest.raises(Exception) as e_info: + assert job.extra == "test" diff --git a/lint-workflow/tests/test_load.py b/lint-workflow/tests/test_load.py index 2374b931..61675090 100644 --- a/lint-workflow/tests/test_load.py +++ b/lint-workflow/tests/test_load.py @@ -15,8 +15,3 @@ def test_load_workflow(workflow_filename): assert workflow.name == "crowdin Pull" assert len(workflow.jobs["crowdin-pull"].steps) == 4 - assert workflow.dict()["name"] == "crowdin Pull" - - #print(workflow.dict(exclude_unset=True)) - - #assert False diff --git a/lint-workflow/tests/test_step.py b/lint-workflow/tests/test_step.py index 426c6d07..dbd460ae 100644 --- a/lint-workflow/tests/test_step.py +++ b/lint-workflow/tests/test_step.py @@ -24,26 +24,24 @@ def test_step_keyword_field(step_default_data): "with": {"config": "test.json"}, **step_default_data } - step = src.models.Step(**data) + step = src.models.Step.from_dict(data) assert "with_field" not in step.json(by_alias=True) assert json.loads(step.json(by_alias=True))["with"] == {"config": "test.json"} @pytest.mark.skip() def test_step_no_keyword_field(step_default_data): - step = src.models.Step(**step_default_data) + step = src.models.Step.from_dict(step_default_data) assert step.with_field == None assert "with_field" not in step.json(by_alias=True) -def test_step_kwargs(step_default_data): - data = { +def test_step_extra_kwargs(step_default_data): + step = src.models.Step.from_dict({ "name": "test step", "extra": "test", **step_default_data - } - step = src.models.Step(**data) - - assert step.extra == "test" - + }) + with pytest.raises(Exception) as e_info: + assert step.extra == "test" diff --git a/lint-workflow/tests/test_workflow.py b/lint-workflow/tests/test_workflow.py index b296219e..0d97fc09 100644 --- a/lint-workflow/tests/test_workflow.py +++ b/lint-workflow/tests/test_workflow.py @@ -26,3 +26,13 @@ def test_workflow_default(workflow_default_data): assert workflow.name == "Test Workflow" assert len(workflow.on.keys()) == 0 assert len(workflow.jobs.keys()) == 1 + + +def test_workflow_extra_kwargs(workflow_default_data): + workflow = src.models.Workflow.from_dict({ + "extra": "test", + **workflow_default_data + }) + + with pytest.raises(Exception) as e_info: + assert workflow.extra == "test" From 87adafd2ba649f28bdc232bf08843c18255c8c75 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 5 May 2023 15:08:04 -0700 Subject: [PATCH 06/57] Got to a somewhat working breakdown of the rules (not including checking the actions) --- lint-workflow/README.md | 2 +- lint-workflow/cli.py | 39 ++++++++++++++++- lint-workflow/src/load.py | 22 +++++++--- lint-workflow/src/rules.py | 62 ++++++++++++++++++++++----- lint-workflow/tests/fixtures/test.yml | 4 +- lint-workflow/tests/test_load.py | 13 +++++- lint-workflow/tests/test_step.py | 1 + 7 files changed, 123 insertions(+), 20 deletions(-) diff --git a/lint-workflow/README.md b/lint-workflow/README.md index 7abf293a..f744c013 100644 --- a/lint-workflow/README.md +++ b/lint-workflow/README.md @@ -43,9 +43,9 @@ workflows: - assert name exists - assert name is capitalized jobs: - - assert runs-on is pinned - assert name exists - assert name is capitalized + - assert runs-on is pinned - assert any environment variables start with "_" steps: shared: diff --git a/lint-workflow/cli.py b/lint-workflow/cli.py index 1d6e82ed..664b5249 100644 --- a/lint-workflow/cli.py +++ b/lint-workflow/cli.py @@ -7,7 +7,8 @@ import logging -from src.rules import LintFinding +from src.rules import LintFinding, workflow_rules, job_rules, step_rules, uses_step_rules, run_step_rules +from src.load import get_workflow PROBLEM_LEVELS = { @@ -196,6 +197,42 @@ def get_action_update(action_id): return update_url +def _new_lint(filename): + findings = [] + max_error_level = 0 + + print(f"Linting: {filename}") + with open(filename) as file: + workflow = get_workflow(filename) + + for rule in workflow_rules: + findings.append(rule.execute(workflow)) + + for job_key, job in workflow.jobs.items(): + for rule in job_rules: + findings.append(rule.execute(job)) + + for step in job.steps: + if step.uses is not None: + for rule in [*step_rules, *uses_step_rules]: + findings.append(rule.execute(step)) + else: + for rule in [*step_rules, *run_step_rules]: + findings.append(rule.execute(step)) + + findings = list(filter(lambda a: a is not None, findings)) + + if len(findings) > 0: + print("#", filename) + for finding in findings: + print_finding(finding) + print() + + max_error_level = get_max_error_level(findings) + + return max_error_level + + def lint(filename): findings = [] diff --git a/lint-workflow/src/load.py b/lint-workflow/src/load.py index e5530c9c..914a43ec 100644 --- a/lint-workflow/src/load.py +++ b/lint-workflow/src/load.py @@ -1,4 +1,5 @@ from ruamel.yaml import YAML +from ruamel.yaml.comments import CommentedMap from .models.job import Job from .models.step import Step @@ -8,15 +9,26 @@ yaml = YAML() -def load_workflow(filename: str) -> Workflow: +def load_workflow(filename: str) -> CommentedMap: with open(filename) as file: - #workflow = sanitize_yaml(file.read()) - workflow = yaml.load(file) + return yaml.load(file) + +def build_workflow(loaded_yaml: str) -> Workflow: return Workflow.from_dict({ - **workflow, + **loaded_yaml, "jobs": {str(job_key): Job.from_dict({ **job, "steps": [Step.from_dict(step) for step in job["steps"]] - }) for job_key, job in workflow["jobs"].items()} + }) for job_key, job in loaded_yaml["jobs"].items()} }) + + +def get_workflow(filename: str) -> Workflow: + return build_workflow(load_workflow(filename)) + + +""" +workflow = load_workflow("tests/fixtures/test.yml") +workflow["jobs"]["crowdin-pull"]["steps"][0]._yaml_comment # has the comment in it +""" diff --git a/lint-workflow/src/rules.py b/lint-workflow/src/rules.py index 2e990f22..18922588 100644 --- a/lint-workflow/src/rules.py +++ b/lint-workflow/src/rules.py @@ -1,22 +1,25 @@ from collections.abc import Callable +from dataclasses import dataclass +from typing import Union from .models.workflow import Workflow from .models.job import Job from .models.step import Step +@dataclass class LintFinding: """Represents a linting problem.""" description: str = "" level: str = None -def _validate( - obj: Workflow | Job | Step, - rule: Callable[[Workflow | Job | Step], bool | None], +def validate( + obj: Union[Workflow, Job, Step], + rule: Callable[Union[Workflow, Job, Step], Union[bool, None]], message: str, warning_level: str -) -> LintFinding | None: +) -> Union[LintFinding, None]: try: if rule(obj): return None @@ -27,18 +30,57 @@ def _validate( return LintFinding(f"{obj.__name__}.{obj.name} => {message}", warning_level) +class Rule: + def __init__( + self, + fn: Callable[Union[Workflow, Job, Step], Union[bool, None]], + message: str = "error", + warning_level: str = "error", + ): + self.fn = fn + self.message = message + self.warning_level = warning_level + + def execute(self, obj: Union[Workflow, Job, Step]): + try: + if self.fn(obj): + return None + except: + message = f"failed to apply {self.fn.__name__}" + warning_level = "error" + + #return LintFinding(f"{obj.__name__}.{obj.name} => {self.message}", self.warning_level) + return LintFinding(f"{obj.name} => {self.message}", self.warning_level) + + # -------- Rules --------- -def workflow_name_exists( obj: Workflow | Job | Step): +def name_exists(obj: Union[Workflow, Job, Step]): return obj.name is not None -def workflow_name_capitalized( obj: Workflow | Job | Step): +def name_capitalized(obj: Union[Workflow, Job, Step]): return obj.name.isupper() + +def step_run_single_line(step: Step): + return True + # ----- End of Rules ----- +workflow_rules = [ + Rule(name_exists, "field required", "error"), + Rule(name_capitalized, "field must be capitalized", "error") +] +job_rules = [ + Rule(name_exists, "field required", "error"), + Rule(name_capitalized, "field must be capitalized", "error") +] +step_rules = [ + Rule(name_exists, "field required", "error"), + Rule(name_capitalized, "field must be capitalized", "error") +] +uses_step_rules = [ +] +run_step_rules = [ +] -findings = list(filter(lambda a: a is not None, [ - _validate(workflow, workflow_name_exists, "field required", "error"), - _validate(workflow, workflow_name_capitalized, "field must be capitalized", "error") -])) diff --git a/lint-workflow/tests/fixtures/test.yml b/lint-workflow/tests/fixtures/test.yml index 595884f5..910a2e3e 100644 --- a/lint-workflow/tests/fixtures/test.yml +++ b/lint-workflow/tests/fixtures/test.yml @@ -15,7 +15,7 @@ jobs: _CROWDIN_PROJECT_ID: "308189" steps: - name: Checkout repo - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.3.4 + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.3.4 - name: Login to Azure uses: Azure/logi@77f1b2e3fb80c0e8645114159d17008b8a2e475a @@ -37,7 +37,7 @@ jobs: done - name: Download translations - uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea # v1.3.2 + uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea # v1.3.2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} diff --git a/lint-workflow/tests/test_load.py b/lint-workflow/tests/test_load.py index 61675090..7ac54171 100644 --- a/lint-workflow/tests/test_load.py +++ b/lint-workflow/tests/test_load.py @@ -11,7 +11,18 @@ def workflow_filename(): def test_load_workflow(workflow_filename): - workflow = src.load.load_workflow(workflow_filename) + loaded_yaml = src.load.load_workflow(workflow_filename) + print(loaded_yaml) + print(type(loaded_yaml)) + print(dir(loaded_yaml)) + + assert False + + +def test_build_workflow(workflow_filename): + workflow = src.load.build_workflow( + src.load.load_workflow(workflow_filename) + ) assert workflow.name == "crowdin Pull" assert len(workflow.jobs["crowdin-pull"].steps) == 4 diff --git a/lint-workflow/tests/test_step.py b/lint-workflow/tests/test_step.py index dbd460ae..c417d67b 100644 --- a/lint-workflow/tests/test_step.py +++ b/lint-workflow/tests/test_step.py @@ -10,6 +10,7 @@ def step_default_data(): "run": "echo \"test\"" } + def test_step_default(step_default_data): step = src.models.Step.from_dict(step_default_data) assert step.name == None From 691cc9f96d9a48c2a0927d9285c38e05c8e7b96a Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 29 Dec 2023 15:31:55 -0800 Subject: [PATCH 07/57] Major additions, test writing, and rule import framework --- lint-workflow/Pipfile | 2 +- lint-workflow/Pipfile.lock | 513 ++++++++++-------- lint-workflow/README.md | 49 +- lint-workflow/Session.vim | 247 +++++++++ lint-workflow/cli.py | 34 +- lint-workflow/flake.lock | 60 ++ lint-workflow/flake.nix | 25 + lint-workflow/settings.py | 4 + lint-workflow/src/load.py | 75 ++- lint-workflow/src/models/job.py | 12 +- lint-workflow/src/models/step.py | 20 +- lint-workflow/src/models/workflow.py | 8 +- lint-workflow/src/rule.py | 46 ++ lint-workflow/src/rules/__init__.py | 0 lint-workflow/src/rules/name_capitalized.py | 17 + lint-workflow/src/rules/name_exists.py | 15 + lint-workflow/src/utils.py | 8 + lint-workflow/tests/OLD_test_action_update.py | 20 + lint-workflow/tests/OLD_test_lint.py | 23 + lint-workflow/tests/OLD_test_main.py | 66 +++ .../tests/OLD_test_workflow_files.py | 37 ++ .../tests/fixtures/test-min-incorrect.yaml | 9 + lint-workflow/tests/fixtures/test-min.yaml | 3 +- lint-workflow/tests/fixtures/test.yml | 3 +- lint-workflow/tests/rules/__init__.py | 0 .../tests/rules/test_name_capitalized.py | 35 ++ lint-workflow/tests/rules/test_name_exists.py | 35 ++ lint-workflow/tests/test_job.py | 24 +- lint-workflow/tests/test_load.py | 19 +- lint-workflow/tests/test_rule.py | 82 +++ lint-workflow/tests/test_step.py | 91 ++-- 31 files changed, 1241 insertions(+), 341 deletions(-) create mode 100644 lint-workflow/Session.vim create mode 100644 lint-workflow/flake.lock create mode 100644 lint-workflow/flake.nix create mode 100644 lint-workflow/settings.py create mode 100644 lint-workflow/src/rule.py create mode 100644 lint-workflow/src/rules/__init__.py create mode 100644 lint-workflow/src/rules/name_capitalized.py create mode 100644 lint-workflow/src/rules/name_exists.py create mode 100644 lint-workflow/src/utils.py create mode 100644 lint-workflow/tests/OLD_test_action_update.py create mode 100644 lint-workflow/tests/OLD_test_lint.py create mode 100644 lint-workflow/tests/OLD_test_main.py create mode 100644 lint-workflow/tests/OLD_test_workflow_files.py create mode 100644 lint-workflow/tests/fixtures/test-min-incorrect.yaml create mode 100644 lint-workflow/tests/rules/__init__.py create mode 100644 lint-workflow/tests/rules/test_name_capitalized.py create mode 100644 lint-workflow/tests/rules/test_name_exists.py create mode 100644 lint-workflow/tests/test_rule.py diff --git a/lint-workflow/Pipfile b/lint-workflow/Pipfile index 88b0b387..06bf5640 100644 --- a/lint-workflow/Pipfile +++ b/lint-workflow/Pipfile @@ -15,4 +15,4 @@ black = "*" pytest = "*" [requires] -python_version = "3.9" +python_version = "3.11" diff --git a/lint-workflow/Pipfile.lock b/lint-workflow/Pipfile.lock index 052ac1c7..2063b47f 100644 --- a/lint-workflow/Pipfile.lock +++ b/lint-workflow/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "a1b4e992db55833a7293809f75176ba5ebc8c68aa29f4ed3b51c6a621cf909b3" + "sha256": "889524a1d31670cc531521bb7ce622380b376403b3c556248a5df8be064fbb64" }, "pipfile-spec": 6, "requires": { - "python_version": "3.9" + "python_version": "3.11" }, "sources": [ { @@ -16,28 +16,29 @@ ] }, "default": { - "dataclasses-json": { + "annotated-types": { "hashes": [ - "sha256:bc285b5f892094c3a53d558858a88553dd6a61a11ab1a8128a0e554385dcc5dd", - "sha256:c2c11bc8214fbf709ffc369d11446ff6945254a7f09128154a7620613d8fda90" + "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43", + "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d" ], - "index": "pypi", - "version": "==0.5.7" + "markers": "python_version >= '3.8'", + "version": "==0.6.0" }, - "marshmallow": { + "dataclasses-json": { "hashes": [ - "sha256:90032c0fd650ce94b6ec6dc8dfeb0e3ff50c144586462c389b81a07205bedb78", - "sha256:93f0958568da045b0021ec6aeb7ac37c81bfcccbb9a0e7ed8559885070b3a19b" + "sha256:35cb40aae824736fdf959801356641836365219cfe14caeb115c39136f775d2a", + "sha256:4aeb343357997396f6bca1acae64e486c3a723d8f5c76301888abeccf0c45176" ], - "markers": "python_version >= '3.7'", - "version": "==3.19.0" + "index": "pypi", + "version": "==0.6.3" }, - "marshmallow-enum": { + "marshmallow": { "hashes": [ - "sha256:38e697e11f45a8e64b4a1e664000897c659b60aa57bfa18d44e226a9920b6e58", - "sha256:57161ab3dbfde4f57adeb12090f39592e992b9c86d206d02f6bd03ebec60f072" + "sha256:5d2371bbe42000f2b3fb5eaa065224df7d8f8597bc19a1bbfa5bfe7fba8da889", + "sha256:684939db93e80ad3561392f47be0230743131560a41c5110684c16e21ade0a5c" ], - "version": "==1.5.1" + "markers": "python_version >= '3.8'", + "version": "==3.20.1" }, "mypy-extensions": { "hashes": [ @@ -49,221 +50,311 @@ }, "packaging": { "hashes": [ - "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", - "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f" + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" ], "markers": "python_version >= '3.7'", - "version": "==23.1" + "version": "==23.2" }, "pydantic": { "hashes": [ - "sha256:01aea3a42c13f2602b7ecbbea484a98169fb568ebd9e247593ea05f01b884b2e", - "sha256:0cd181f1d0b1d00e2b705f1bf1ac7799a2d938cce3376b8007df62b29be3c2c6", - "sha256:10a86d8c8db68086f1e30a530f7d5f83eb0685e632e411dbbcf2d5c0150e8dcd", - "sha256:193924c563fae6ddcb71d3f06fa153866423ac1b793a47936656e806b64e24ca", - "sha256:464855a7ff7f2cc2cf537ecc421291b9132aa9c79aef44e917ad711b4a93163b", - "sha256:516f1ed9bc2406a0467dd777afc636c7091d71f214d5e413d64fef45174cfc7a", - "sha256:6434b49c0b03a51021ade5c4daa7d70c98f7a79e95b551201fff682fc1661245", - "sha256:64d34ab766fa056df49013bb6e79921a0265204c071984e75a09cbceacbbdd5d", - "sha256:670bb4683ad1e48b0ecb06f0cfe2178dcf74ff27921cdf1606e527d2617a81ee", - "sha256:68792151e174a4aa9e9fc1b4e653e65a354a2fa0fed169f7b3d09902ad2cb6f1", - "sha256:701daea9ffe9d26f97b52f1d157e0d4121644f0fcf80b443248434958fd03dc3", - "sha256:7d45fc99d64af9aaf7e308054a0067fdcd87ffe974f2442312372dfa66e1001d", - "sha256:80b1fab4deb08a8292d15e43a6edccdffa5377a36a4597bb545b93e79c5ff0a5", - "sha256:82dffb306dd20bd5268fd6379bc4bfe75242a9c2b79fec58e1041fbbdb1f7914", - "sha256:8c7f51861d73e8b9ddcb9916ae7ac39fb52761d9ea0df41128e81e2ba42886cd", - "sha256:950ce33857841f9a337ce07ddf46bc84e1c4946d2a3bba18f8280297157a3fd1", - "sha256:976cae77ba6a49d80f461fd8bba183ff7ba79f44aa5cfa82f1346b5626542f8e", - "sha256:9f6f0fd68d73257ad6685419478c5aece46432f4bdd8d32c7345f1986496171e", - "sha256:a7cd2251439988b413cb0a985c4ed82b6c6aac382dbaff53ae03c4b23a70e80a", - "sha256:abfb7d4a7cd5cc4e1d1887c43503a7c5dd608eadf8bc615413fc498d3e4645cd", - "sha256:ae150a63564929c675d7f2303008d88426a0add46efd76c3fc797cd71cb1b46f", - "sha256:b0f85904f73161817b80781cc150f8b906d521fa11e3cdabae19a581c3606209", - "sha256:b4a849d10f211389502059c33332e91327bc154acc1845f375a99eca3afa802d", - "sha256:c15582f9055fbc1bfe50266a19771bbbef33dd28c45e78afbe1996fd70966c2a", - "sha256:c230c0d8a322276d6e7b88c3f7ce885f9ed16e0910354510e0bae84d54991143", - "sha256:cc1dde4e50a5fc1336ee0581c1612215bc64ed6d28d2c7c6f25d2fe3e7c3e918", - "sha256:cf135c46099ff3f919d2150a948ce94b9ce545598ef2c6c7bf55dca98a304b52", - "sha256:cfc83c0678b6ba51b0532bea66860617c4cd4251ecf76e9846fa5a9f3454e97e", - "sha256:d2a5ebb48958754d386195fe9e9c5106f11275867051bf017a8059410e9abf1f", - "sha256:d71e69699498b020ea198468e2480a2f1e7433e32a3a99760058c6520e2bea7e", - "sha256:d75ae19d2a3dbb146b6f324031c24f8a3f52ff5d6a9f22f0683694b3afcb16fb", - "sha256:dfe2507b8ef209da71b6fb5f4e597b50c5a34b78d7e857c4f8f3115effaef5fe", - "sha256:e0cfe895a504c060e5d36b287ee696e2fdad02d89e0d895f83037245218a87fe", - "sha256:e79e999e539872e903767c417c897e729e015872040e56b96e67968c3b918b2d", - "sha256:ecbbc51391248116c0a055899e6c3e7ffbb11fb5e2a4cd6f2d0b93272118a209", - "sha256:f4a2b50e2b03d5776e7f21af73e2070e1b5c0d0df255a827e7c632962f8315af" + "sha256:b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a", + "sha256:d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4" ], "index": "pypi", - "version": "==1.10.7" + "version": "==2.5.3" + }, + "pydantic-core": { + "hashes": [ + "sha256:00646784f6cd993b1e1c0e7b0fdcbccc375d539db95555477771c27555e3c556", + "sha256:00b1087dabcee0b0ffd104f9f53d7d3eaddfaa314cdd6726143af6bc713aa27e", + "sha256:0348b1dc6b76041516e8a854ff95b21c55f5a411c3297d2ca52f5528e49d8411", + "sha256:036137b5ad0cb0004c75b579445a1efccd072387a36c7f217bb8efd1afbe5245", + "sha256:095b707bb287bfd534044166ab767bec70a9bba3175dcdc3371782175c14e43c", + "sha256:0c08de15d50fa190d577e8591f0329a643eeaed696d7771760295998aca6bc66", + "sha256:1302a54f87b5cd8528e4d6d1bf2133b6aa7c6122ff8e9dc5220fbc1e07bffebd", + "sha256:172de779e2a153d36ee690dbc49c6db568d7b33b18dc56b69a7514aecbcf380d", + "sha256:1b027c86c66b8627eb90e57aee1f526df77dc6d8b354ec498be9a757d513b92b", + "sha256:1ce830e480f6774608dedfd4a90c42aac4a7af0a711f1b52f807130c2e434c06", + "sha256:1fd0c1d395372843fba13a51c28e3bb9d59bd7aebfeb17358ffaaa1e4dbbe948", + "sha256:23598acb8ccaa3d1d875ef3b35cb6376535095e9405d91a3d57a8c7db5d29341", + "sha256:24368e31be2c88bd69340fbfe741b405302993242ccb476c5c3ff48aeee1afe0", + "sha256:26a92ae76f75d1915806b77cf459811e772d8f71fd1e4339c99750f0e7f6324f", + "sha256:27e524624eace5c59af499cd97dc18bb201dc6a7a2da24bfc66ef151c69a5f2a", + "sha256:2b8719037e570639e6b665a4050add43134d80b687288ba3ade18b22bbb29dd2", + "sha256:2c5bcf3414367e29f83fd66f7de64509a8fd2368b1edf4351e862910727d3e51", + "sha256:2dbe357bc4ddda078f79d2a36fc1dd0494a7f2fad83a0a684465b6f24b46fe80", + "sha256:2f5fa187bde8524b1e37ba894db13aadd64faa884657473b03a019f625cee9a8", + "sha256:2f6ffc6701a0eb28648c845f4945a194dc7ab3c651f535b81793251e1185ac3d", + "sha256:314ccc4264ce7d854941231cf71b592e30d8d368a71e50197c905874feacc8a8", + "sha256:36026d8f99c58d7044413e1b819a67ca0e0b8ebe0f25e775e6c3d1fabb3c38fb", + "sha256:36099c69f6b14fc2c49d7996cbf4f87ec4f0e66d1c74aa05228583225a07b590", + "sha256:36fa402dcdc8ea7f1b0ddcf0df4254cc6b2e08f8cd80e7010d4c4ae6e86b2a87", + "sha256:370ffecb5316ed23b667d99ce4debe53ea664b99cc37bfa2af47bc769056d534", + "sha256:3860c62057acd95cc84044e758e47b18dcd8871a328ebc8ccdefd18b0d26a21b", + "sha256:399ac0891c284fa8eb998bcfa323f2234858f5d2efca3950ae58c8f88830f145", + "sha256:3a0b5db001b98e1c649dd55afa928e75aa4087e587b9524a4992316fa23c9fba", + "sha256:3dcf1978be02153c6a31692d4fbcc2a3f1db9da36039ead23173bc256ee3b91b", + "sha256:4241204e4b36ab5ae466ecec5c4c16527a054c69f99bba20f6f75232a6a534e2", + "sha256:438027a975cc213a47c5d70672e0d29776082155cfae540c4e225716586be75e", + "sha256:43e166ad47ba900f2542a80d83f9fc65fe99eb63ceec4debec160ae729824052", + "sha256:478e9e7b360dfec451daafe286998d4a1eeaecf6d69c427b834ae771cad4b622", + "sha256:4ce8299b481bcb68e5c82002b96e411796b844d72b3e92a3fbedfe8e19813eab", + "sha256:4f86f1f318e56f5cbb282fe61eb84767aee743ebe32c7c0834690ebea50c0a6b", + "sha256:55a23dcd98c858c0db44fc5c04fc7ed81c4b4d33c653a7c45ddaebf6563a2f66", + "sha256:599c87d79cab2a6a2a9df4aefe0455e61e7d2aeede2f8577c1b7c0aec643ee8e", + "sha256:5aa90562bc079c6c290f0512b21768967f9968e4cfea84ea4ff5af5d917016e4", + "sha256:64634ccf9d671c6be242a664a33c4acf12882670b09b3f163cd00a24cffbd74e", + "sha256:667aa2eac9cd0700af1ddb38b7b1ef246d8cf94c85637cbb03d7757ca4c3fdec", + "sha256:6a31d98c0d69776c2576dda4b77b8e0c69ad08e8b539c25c7d0ca0dc19a50d6c", + "sha256:6af4b3f52cc65f8a0bc8b1cd9676f8c21ef3e9132f21fed250f6958bd7223bed", + "sha256:6c8edaea3089bf908dd27da8f5d9e395c5b4dc092dbcce9b65e7156099b4b937", + "sha256:71d72ca5eaaa8d38c8df16b7deb1a2da4f650c41b58bb142f3fb75d5ad4a611f", + "sha256:72f9a942d739f09cd42fffe5dc759928217649f070056f03c70df14f5770acf9", + "sha256:747265448cb57a9f37572a488a57d873fd96bf51e5bb7edb52cfb37124516da4", + "sha256:75ec284328b60a4e91010c1acade0c30584f28a1f345bc8f72fe8b9e46ec6a96", + "sha256:78d0768ee59baa3de0f4adac9e3748b4b1fffc52143caebddfd5ea2961595277", + "sha256:78ee52ecc088c61cce32b2d30a826f929e1708f7b9247dc3b921aec367dc1b23", + "sha256:7be719e4d2ae6c314f72844ba9d69e38dff342bc360379f7c8537c48e23034b7", + "sha256:7e1f4744eea1501404b20b0ac059ff7e3f96a97d3e3f48ce27a139e053bb370b", + "sha256:7e90d6cc4aad2cc1f5e16ed56e46cebf4877c62403a311af20459c15da76fd91", + "sha256:7ebe3416785f65c28f4f9441e916bfc8a54179c8dea73c23023f7086fa601c5d", + "sha256:7f41533d7e3cf9520065f610b41ac1c76bc2161415955fbcead4981b22c7611e", + "sha256:7f5025db12fc6de7bc1104d826d5aee1d172f9ba6ca936bf6474c2148ac336c1", + "sha256:86c963186ca5e50d5c8287b1d1c9d3f8f024cbe343d048c5bd282aec2d8641f2", + "sha256:86ce5fcfc3accf3a07a729779d0b86c5d0309a4764c897d86c11089be61da160", + "sha256:8a14c192c1d724c3acbfb3f10a958c55a2638391319ce8078cb36c02283959b9", + "sha256:8b93785eadaef932e4fe9c6e12ba67beb1b3f1e5495631419c784ab87e975670", + "sha256:8ed1af8692bd8d2a29d702f1a2e6065416d76897d726e45a1775b1444f5928a7", + "sha256:92879bce89f91f4b2416eba4429c7b5ca22c45ef4a499c39f0c5c69257522c7c", + "sha256:94fc0e6621e07d1e91c44e016cc0b189b48db053061cc22d6298a611de8071bb", + "sha256:982487f8931067a32e72d40ab6b47b1628a9c5d344be7f1a4e668fb462d2da42", + "sha256:9862bf828112e19685b76ca499b379338fd4c5c269d897e218b2ae8fcb80139d", + "sha256:99b14dbea2fdb563d8b5a57c9badfcd72083f6006caf8e126b491519c7d64ca8", + "sha256:9c6a5c79b28003543db3ba67d1df336f253a87d3112dac3a51b94f7d48e4c0e1", + "sha256:a19b794f8fe6569472ff77602437ec4430f9b2b9ec7a1105cfd2232f9ba355e6", + "sha256:a306cdd2ad3a7d795d8e617a58c3a2ed0f76c8496fb7621b6cd514eb1532cae8", + "sha256:a3dde6cac75e0b0902778978d3b1646ca9f438654395a362cb21d9ad34b24acf", + "sha256:a874f21f87c485310944b2b2734cd6d318765bcbb7515eead33af9641816506e", + "sha256:a983cca5ed1dd9a35e9e42ebf9f278d344603bfcb174ff99a5815f953925140a", + "sha256:aca48506a9c20f68ee61c87f2008f81f8ee99f8d7f0104bff3c47e2d148f89d9", + "sha256:b2602177668f89b38b9f84b7b3435d0a72511ddef45dc14446811759b82235a1", + "sha256:b3e5fe4538001bb82e2295b8d2a39356a84694c97cb73a566dc36328b9f83b40", + "sha256:b6ca36c12a5120bad343eef193cc0122928c5c7466121da7c20f41160ba00ba2", + "sha256:b89f4477d915ea43b4ceea6756f63f0288941b6443a2b28c69004fe07fde0d0d", + "sha256:b9a9d92f10772d2a181b5ca339dee066ab7d1c9a34ae2421b2a52556e719756f", + "sha256:c99462ffc538717b3e60151dfaf91125f637e801f5ab008f81c402f1dff0cd0f", + "sha256:cb92f9061657287eded380d7dc455bbf115430b3aa4741bdc662d02977e7d0af", + "sha256:cdee837710ef6b56ebd20245b83799fce40b265b3b406e51e8ccc5b85b9099b7", + "sha256:cf10b7d58ae4a1f07fccbf4a0a956d705356fea05fb4c70608bb6fa81d103cda", + "sha256:d15687d7d7f40333bd8266f3814c591c2e2cd263fa2116e314f60d82086e353a", + "sha256:d5c28525c19f5bb1e09511669bb57353d22b94cf8b65f3a8d141c389a55dec95", + "sha256:d5f916acf8afbcab6bacbb376ba7dc61f845367901ecd5e328fc4d4aef2fcab0", + "sha256:dab03ed811ed1c71d700ed08bde8431cf429bbe59e423394f0f4055f1ca0ea60", + "sha256:db453f2da3f59a348f514cfbfeb042393b68720787bbef2b4c6068ea362c8149", + "sha256:de2a0645a923ba57c5527497daf8ec5df69c6eadf869e9cd46e86349146e5975", + "sha256:dea7fcd62915fb150cdc373212141a30037e11b761fbced340e9db3379b892d4", + "sha256:dfcbebdb3c4b6f739a91769aea5ed615023f3c88cb70df812849aef634c25fbe", + "sha256:dfcebb950aa7e667ec226a442722134539e77c575f6cfaa423f24371bb8d2e94", + "sha256:e0641b506486f0b4cd1500a2a65740243e8670a2549bb02bc4556a83af84ae03", + "sha256:e33b0834f1cf779aa839975f9d8755a7c2420510c0fa1e9fa0497de77cd35d2c", + "sha256:e4ace1e220b078c8e48e82c081e35002038657e4b37d403ce940fa679e57113b", + "sha256:e4cf2d5829f6963a5483ec01578ee76d329eb5caf330ecd05b3edd697e7d768a", + "sha256:e574de99d735b3fc8364cba9912c2bec2da78775eba95cbb225ef7dda6acea24", + "sha256:e646c0e282e960345314f42f2cea5e0b5f56938c093541ea6dbf11aec2862391", + "sha256:e8a5ac97ea521d7bde7621d86c30e86b798cdecd985723c4ed737a2aa9e77d0c", + "sha256:eedf97be7bc3dbc8addcef4142f4b4164066df0c6f36397ae4aaed3eb187d8ab", + "sha256:ef633add81832f4b56d3b4c9408b43d530dfca29e68fb1b797dcb861a2c734cd", + "sha256:f27207e8ca3e5e021e2402ba942e5b4c629718e665c81b8b306f3c8b1ddbb786", + "sha256:f85f3843bdb1fe80e8c206fe6eed7a1caeae897e496542cee499c374a85c6e08", + "sha256:f8e81e4b55930e5ffab4a68db1af431629cf2e4066dbdbfef65348b8ab804ea8", + "sha256:f96ae96a060a8072ceff4cfde89d261837b4294a4f28b84a28765470d502ccc6", + "sha256:fd9e98b408384989ea4ab60206b8e100d8687da18b5c813c11e92fd8212a98e0", + "sha256:ffff855100bc066ff2cd3aa4a60bc9534661816b110f0243e59503ec2df38421" + ], + "markers": "python_version >= '3.7'", + "version": "==2.14.6" }, "pyyaml": { "hashes": [ - "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", - "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", - "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", - "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", - "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", - "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", - "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", - "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", - "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", - "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", - "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", - "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", - "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782", - "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", - "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", - "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", - "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", - "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", - "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1", - "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", - "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", - "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", - "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", - "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", - "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", - "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d", - "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", - "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", - "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7", - "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", - "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", - "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", - "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358", - "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", - "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", - "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", - "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", - "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f", - "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", - "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" + "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", + "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", + "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", + "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", + "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", + "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", + "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", + "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", + "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", + "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", + "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", + "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", + "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", + "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", + "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", + "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", + "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", + "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", + "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", + "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", + "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", + "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", + "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", + "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", + "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", + "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", + "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", + "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", + "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", + "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", + "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", + "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", + "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", + "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", + "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", + "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", + "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", + "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", + "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", + "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", + "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", + "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", + "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", + "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", + "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", + "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", + "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", + "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", + "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", + "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" ], "index": "pypi", - "version": "==6.0" + "version": "==6.0.1" }, "ruamel.yaml": { "hashes": [ - "sha256:40e549c264e799d8e4cd9e54e869ffa60c9436e1ef497500b08fbda3b92a1517", - "sha256:605510839d5af3ae730f3ed6408b7004c83f97aa2b732f6f366da7c2fc2f0d9c" + "sha256:61917e3a35a569c1133a8f772e1226961bf5a1198bea7e23f06a0841dea1ab0e", + "sha256:a013ac02f99a69cdd6277d9664689eb1acba07069f912823177c5eced21a6ada" ], "index": "pypi", - "version": "==0.17.23" + "version": "==0.18.5" }, "ruamel.yaml.clib": { "hashes": [ - "sha256:045e0626baf1c52e5527bd5db361bc83180faaba2ff586e763d3d5982a876a9e", - "sha256:15910ef4f3e537eea7fe45f8a5d19997479940d9196f357152a09031c5be59f3", - "sha256:184faeaec61dbaa3cace407cffc5819f7b977e75360e8d5ca19461cd851a5fc5", - "sha256:1f08fd5a2bea9c4180db71678e850b995d2a5f4537be0e94557668cf0f5f9497", - "sha256:2aa261c29a5545adfef9296b7e33941f46aa5bbd21164228e833412af4c9c75f", - "sha256:3110a99e0f94a4a3470ff67fc20d3f96c25b13d24c6980ff841e82bafe827cac", - "sha256:3243f48ecd450eddadc2d11b5feb08aca941b5cd98c9b1db14b2fd128be8c697", - "sha256:370445fd795706fd291ab00c9df38a0caed0f17a6fb46b0f607668ecb16ce763", - "sha256:40d030e2329ce5286d6b231b8726959ebbe0404c92f0a578c0e2482182e38282", - "sha256:41d0f1fa4c6830176eef5b276af04c89320ea616655d01327d5ce65e50575c94", - "sha256:4a4d8d417868d68b979076a9be6a38c676eca060785abaa6709c7b31593c35d1", - "sha256:4b3a93bb9bc662fc1f99c5c3ea8e623d8b23ad22f861eb6fce9377ac07ad6072", - "sha256:5bc0667c1eb8f83a3752b71b9c4ba55ef7c7058ae57022dd9b29065186a113d9", - "sha256:721bc4ba4525f53f6a611ec0967bdcee61b31df5a56801281027a3a6d1c2daf5", - "sha256:763d65baa3b952479c4e972669f679fe490eee058d5aa85da483ebae2009d231", - "sha256:7bdb4c06b063f6fd55e472e201317a3bb6cdeeee5d5a38512ea5c01e1acbdd93", - "sha256:8831a2cedcd0f0927f788c5bdf6567d9dc9cc235646a434986a852af1cb54b4b", - "sha256:91a789b4aa0097b78c93e3dc4b40040ba55bef518f84a40d4442f713b4094acb", - "sha256:92460ce908546ab69770b2e576e4f99fbb4ce6ab4b245345a3869a0a0410488f", - "sha256:99e77daab5d13a48a4054803d052ff40780278240a902b880dd37a51ba01a307", - "sha256:a234a20ae07e8469da311e182e70ef6b199d0fbeb6c6cc2901204dd87fb867e8", - "sha256:a7b301ff08055d73223058b5c46c55638917f04d21577c95e00e0c4d79201a6b", - "sha256:be2a7ad8fd8f7442b24323d24ba0b56c51219513cfa45b9ada3b87b76c374d4b", - "sha256:bf9a6bc4a0221538b1a7de3ed7bca4c93c02346853f44e1cd764be0023cd3640", - "sha256:c3ca1fbba4ae962521e5eb66d72998b51f0f4d0f608d3c0347a48e1af262efa7", - "sha256:d000f258cf42fec2b1bbf2863c61d7b8918d31ffee905da62dede869254d3b8a", - "sha256:d5859983f26d8cd7bb5c287ef452e8aacc86501487634573d260968f753e1d71", - "sha256:d5e51e2901ec2366b79f16c2299a03e74ba4531ddcfacc1416639c557aef0ad8", - "sha256:da538167284de58a52109a9b89b8f6a53ff8437dd6dc26d33b57bf6699153122", - "sha256:debc87a9516b237d0466a711b18b6ebeb17ba9f391eb7f91c649c5c4ec5006c7", - "sha256:df5828871e6648db72d1c19b4bd24819b80a755c4541d3409f0f7acd0f335c80", - "sha256:ecdf1a604009bd35c674b9225a8fa609e0282d9b896c03dd441a91e5f53b534e", - "sha256:efa08d63ef03d079dcae1dfe334f6c8847ba8b645d08df286358b1f5293d24ab", - "sha256:f01da5790e95815eb5a8a138508c01c758e5f5bc0ce4286c4f7028b8dd7ac3d0", - "sha256:f34019dced51047d6f70cb9383b2ae2853b7fc4dce65129a5acd49f4f9256646", - "sha256:f6d3d39611ac2e4f62c3128a9eed45f19a6608670c5a2f4f07f24e8de3441d38" + "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d", + "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001", + "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462", + "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9", + "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe", + "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b", + "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b", + "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615", + "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62", + "sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15", + "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b", + "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1", + "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9", + "sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675", + "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899", + "sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7", + "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7", + "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312", + "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa", + "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91", + "sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b", + "sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6", + "sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3", + "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334", + "sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5", + "sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3", + "sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe", + "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c", + "sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed", + "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337", + "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880", + "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f", + "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d", + "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248", + "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d", + "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf", + "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512", + "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069", + "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb", + "sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942", + "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d", + "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31", + "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92", + "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5", + "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28", + "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d", + "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1", + "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2", + "sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875", + "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412" ], - "markers": "python_version < '3.12' and platform_python_implementation == 'CPython'", - "version": "==0.2.7" + "markers": "python_version < '3.13' and platform_python_implementation == 'CPython'", + "version": "==0.2.8" }, "typing-extensions": { "hashes": [ - "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb", - "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4" + "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", + "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" ], - "markers": "python_version >= '3.7'", - "version": "==4.5.0" + "markers": "python_version >= '3.8'", + "version": "==4.9.0" }, "typing-inspect": { "hashes": [ - "sha256:5fbf9c1e65d4fa01e701fe12a5bca6c6e08a4ffd5bc60bfac028253a447c5188", - "sha256:8b1ff0c400943b6145df8119c41c244ca8207f1f10c9c057aeed1560e4806e3d" + "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", + "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78" ], - "version": "==0.8.0" + "version": "==0.9.0" }, "urllib3": { "hashes": [ - "sha256:61717a1095d7e155cdb737ac7bb2f4324a858a1e2e6466f6d03ff630ca68d3cc", - "sha256:d055c2f9d38dc53c808f6fdc8eab7360b6fdbbde02340ed25cfbcd817c62469e" + "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", + "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54" ], "index": "pypi", - "version": "==2.0.2" + "version": "==2.1.0" } }, "develop": { "black": { "hashes": [ - "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5", - "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915", - "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326", - "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940", - "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b", - "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30", - "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c", - "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c", - "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab", - "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27", - "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2", - "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961", - "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9", - "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb", - "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70", - "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331", - "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2", - "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266", - "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d", - "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6", - "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b", - "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925", - "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8", - "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4", - "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3" + "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50", + "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f", + "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e", + "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec", + "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055", + "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3", + "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5", + "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54", + "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b", + "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e", + "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e", + "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba", + "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea", + "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59", + "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d", + "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0", + "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9", + "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a", + "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e", + "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba", + "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2", + "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2" ], "index": "pypi", - "version": "==23.3.0" + "version": "==23.12.1" }, "click": { "hashes": [ - "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", - "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" ], "markers": "python_version >= '3.7'", - "version": "==8.1.3" - }, - "exceptiongroup": { - "hashes": [ - "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e", - "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785" - ], - "markers": "python_version < '3.11'", - "version": "==1.1.1" + "version": "==8.1.7" }, "iniconfig": { "hashes": [ @@ -283,59 +374,43 @@ }, "packaging": { "hashes": [ - "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", - "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f" + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" ], "markers": "python_version >= '3.7'", - "version": "==23.1" + "version": "==23.2" }, "pathspec": { "hashes": [ - "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687", - "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293" + "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", + "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" ], - "markers": "python_version >= '3.7'", - "version": "==0.11.1" + "markers": "python_version >= '3.8'", + "version": "==0.12.1" }, "platformdirs": { "hashes": [ - "sha256:47692bc24c1958e8b0f13dd727307cff1db103fca36399f457da8e05f222fdc4", - "sha256:7954a68d0ba23558d753f73437c55f89027cf8f5108c19844d4b82e5af396335" + "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380", + "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420" ], - "markers": "python_version >= '3.7'", - "version": "==3.5.0" + "markers": "python_version >= '3.8'", + "version": "==4.1.0" }, "pluggy": { "hashes": [ - "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", - "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" + "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12", + "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7" ], - "markers": "python_version >= '3.6'", - "version": "==1.0.0" + "markers": "python_version >= '3.8'", + "version": "==1.3.0" }, "pytest": { "hashes": [ - "sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362", - "sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3" + "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac", + "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5" ], "index": "pypi", - "version": "==7.3.1" - }, - "tomli": { - "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" - ], - "markers": "python_version < '3.11'", - "version": "==2.0.1" - }, - "typing-extensions": { - "hashes": [ - "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb", - "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4" - ], - "markers": "python_version >= '3.7'", - "version": "==4.5.0" + "version": "==7.4.3" } } } diff --git a/lint-workflow/README.md b/lint-workflow/README.md index f744c013..db9da62b 100644 --- a/lint-workflow/README.md +++ b/lint-workflow/README.md @@ -3,7 +3,7 @@ ## Development ### Requirements -- Python 3.9 +- Python 3.11 - pipenv ### Setup @@ -38,26 +38,31 @@ black . ### Rules -```yaml -workflows: - - assert name exists - - assert name is capitalized -jobs: - - assert name exists - - assert name is capitalized - - assert runs-on is pinned - - assert any environment variables start with "_" -steps: - shared: - - assert name exists - - assert name is capitilized - uses: - - assert valid hash format - correct length - - assert valid hash format - cast to hexidecimal - - assert valid action repo path format - - assert action exists in GitHub - - warn out of date Action - run: - - assert correct format for single line run +#### workflows + +- [x] assert name exists +- [x] assert name is capitalized + +#### jobs + +- [x] assert name exists +- [x] assert name is capitalized +- [ ] assert runs-on is pinned +- [ ] assert any environment variables start with "_" + +#### shared steps +- [x] assert name exists +- [x] assert name is capitilized + +#### uses steps +- [ ] assert valid hash format - correct length +- [ ] assert valid hash format - cast to hexidecimal +- [ ] assert valid action repo path format +- [ ] assert action exists in GitHub +- [ ] warn out of date Action +- [ ] warn using an unapproved Action + +#### run steps +- [ ] assert correct format for single line run ``` diff --git a/lint-workflow/Session.vim b/lint-workflow/Session.vim new file mode 100644 index 00000000..6ad2b247 --- /dev/null +++ b/lint-workflow/Session.vim @@ -0,0 +1,247 @@ +let SessionLoad = 1 +let s:so_save = &g:so | let s:siso_save = &g:siso | setg so=0 siso=0 | setl so=-1 siso=-1 +let v:this_session=expand(":p") +silent only +silent tabonly +cd ~/projects/workflow-linter +if expand('%') == '' && !&modified && line('$') <= 1 && getline(1) == '' + let s:wipebuf = bufnr('%') +endif +let s:shortmess_save = &shortmess +if &shortmess =~ 'A' + set shortmess=aoOA +else + set shortmess=aoO +endif +badd +1 README.md +badd +211 cli.py +badd +1 src/load.py +badd +1 config.yaml +badd +1 src/rules/__init__.py +badd +13 src/rules/name_capitalized.py +badd +11 src/rules/name_exists.py +badd +1 src/rule.py +badd +0 tests/test_rule.py +badd +35 tests/rules/test_name_exists.py +badd +35 tests/rules/test_name_capitalized.py +badd +4 rule_settings.py +badd +3 settings.py +badd +0 tests/fixtures/test.yml +argglobal +%argdel +$argadd README.md +set stal=2 +tabnew +setlocal\ bufhidden=wipe +tabnew +setlocal\ bufhidden=wipe +tabrewind +edit README.md +let s:save_splitbelow = &splitbelow +let s:save_splitright = &splitright +set splitbelow splitright +wincmd _ | wincmd | +vsplit +1wincmd h +wincmd w +let &splitbelow = s:save_splitbelow +let &splitright = s:save_splitright +wincmd t +let s:save_winminheight = &winminheight +let s:save_winminwidth = &winminwidth +set winminheight=0 +set winheight=1 +set winminwidth=0 +set winwidth=1 +exe 'vert 1resize ' . ((&columns * 163 + 163) / 327) +exe 'vert 2resize ' . ((&columns * 163 + 163) / 327) +argglobal +balt tests/fixtures/test.yml +setlocal fdm=manual +setlocal fde=0 +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=0 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +silent! normal! zE +let &fdl = &fdl +let s:l = 10 - ((9 * winheight(0) + 32) / 65) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 10 +normal! 0 +wincmd w +argglobal +if bufexists(fnamemodify("settings.py", ":p")) | buffer settings.py | else | edit settings.py | endif +if &buftype ==# 'terminal' + silent file settings.py +endif +balt cli.py +setlocal fdm=manual +setlocal fde=0 +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=0 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +silent! normal! zE +let &fdl = &fdl +let s:l = 2 - ((1 * winheight(0) + 32) / 65) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 2 +normal! 06| +wincmd w +exe 'vert 1resize ' . ((&columns * 163 + 163) / 327) +exe 'vert 2resize ' . ((&columns * 163 + 163) / 327) +tabnext +edit cli.py +let s:save_splitbelow = &splitbelow +let s:save_splitright = &splitright +set splitbelow splitright +wincmd _ | wincmd | +vsplit +1wincmd h +wincmd w +let &splitbelow = s:save_splitbelow +let &splitright = s:save_splitright +wincmd t +let s:save_winminheight = &winminheight +let s:save_winminwidth = &winminwidth +set winminheight=0 +set winheight=1 +set winminwidth=0 +set winwidth=1 +exe 'vert 1resize ' . ((&columns * 163 + 163) / 327) +exe 'vert 2resize ' . ((&columns * 163 + 163) / 327) +argglobal +balt settings.py +setlocal fdm=manual +setlocal fde=0 +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=0 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +silent! normal! zE +let &fdl = &fdl +let s:l = 3 - ((2 * winheight(0) + 32) / 65) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 3 +normal! 0 +wincmd w +argglobal +if bufexists(fnamemodify("src/load.py", ":p")) | buffer src/load.py | else | edit src/load.py | endif +if &buftype ==# 'terminal' + silent file src/load.py +endif +balt settings.py +setlocal fdm=manual +setlocal fde=0 +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=0 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +silent! normal! zE +let &fdl = &fdl +let s:l = 34 - ((21 * winheight(0) + 32) / 65) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 34 +normal! 0 +wincmd w +2wincmd w +exe 'vert 1resize ' . ((&columns * 163 + 163) / 327) +exe 'vert 2resize ' . ((&columns * 163 + 163) / 327) +tabnext +edit src/rule.py +let s:save_splitbelow = &splitbelow +let s:save_splitright = &splitright +set splitbelow splitright +wincmd _ | wincmd | +vsplit +1wincmd h +wincmd w +let &splitbelow = s:save_splitbelow +let &splitright = s:save_splitright +wincmd t +let s:save_winminheight = &winminheight +let s:save_winminwidth = &winminwidth +set winminheight=0 +set winheight=1 +set winminwidth=0 +set winwidth=1 +exe 'vert 1resize ' . ((&columns * 163 + 163) / 327) +exe 'vert 2resize ' . ((&columns * 163 + 163) / 327) +argglobal +balt tests/test_rule.py +setlocal fdm=manual +setlocal fde=0 +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=0 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +silent! normal! zE +let &fdl = &fdl +let s:l = 32 - ((31 * winheight(0) + 32) / 65) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 32 +normal! 017| +wincmd w +argglobal +if bufexists(fnamemodify("tests/test_rule.py", ":p")) | buffer tests/test_rule.py | else | edit tests/test_rule.py | endif +if &buftype ==# 'terminal' + silent file tests/test_rule.py +endif +balt src/rule.py +setlocal fdm=manual +setlocal fde=0 +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=0 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +silent! normal! zE +let &fdl = &fdl +let s:l = 82 - ((64 * winheight(0) + 32) / 65) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 82 +normal! 053| +wincmd w +exe 'vert 1resize ' . ((&columns * 163 + 163) / 327) +exe 'vert 2resize ' . ((&columns * 163 + 163) / 327) +tabnext 2 +set stal=1 +if exists('s:wipebuf') && len(win_findbuf(s:wipebuf)) == 0 && getbufvar(s:wipebuf, '&buftype') isnot# 'terminal' + silent exe 'bwipe ' . s:wipebuf +endif +unlet! s:wipebuf +set winheight=1 winwidth=20 +let &shortmess = s:shortmess_save +let &winminheight = s:save_winminheight +let &winminwidth = s:save_winminwidth +let s:sx = expand(":p:r")."x.vim" +if filereadable(s:sx) + exe "source " . fnameescape(s:sx) +endif +let &g:so = s:so_save | let &g:siso = s:siso_save +set hlsearch +doautoall SessionLoadPost +unlet SessionLoad +" vim: set ft=vim : diff --git a/lint-workflow/cli.py b/lint-workflow/cli.py index 664b5249..420a0544 100644 --- a/lint-workflow/cli.py +++ b/lint-workflow/cli.py @@ -6,9 +6,10 @@ import urllib3 as urllib import logging - -from src.rules import LintFinding, workflow_rules, job_rules, step_rules, uses_step_rules, run_step_rules -from src.load import get_workflow +#from src.rules import workflow_rules, job_rules, step_rules, uses_step_rules, run_step_rules +from settings import enabled_rules +from src.load import WorkflowBuilder, Rules +from src.utils import LintFinding PROBLEM_LEVELS = { @@ -19,6 +20,11 @@ memoized_action_update_urls = {} +lint_rules = Rules(enabled_rules, verbose=True) + +#print(lint_rules.workflow) + + class Colors: """Class containing color codes for printing strings to output.""" @@ -203,22 +209,18 @@ def _new_lint(filename): print(f"Linting: {filename}") with open(filename) as file: - workflow = get_workflow(filename) + workflow = WorkflowBuilder.build(filename) - for rule in workflow_rules: + for rule in lint_rules.workflow: findings.append(rule.execute(workflow)) for job_key, job in workflow.jobs.items(): - for rule in job_rules: + for rule in lint_rules.job: findings.append(rule.execute(job)) for step in job.steps: - if step.uses is not None: - for rule in [*step_rules, *uses_step_rules]: - findings.append(rule.execute(step)) - else: - for rule in [*step_rules, *run_step_rules]: - findings.append(rule.execute(step)) + for rule in lint_rules.step: + findings.append(rule.execute(step)) findings = list(filter(lambda a: a is not None, findings)) @@ -234,6 +236,10 @@ def _new_lint(filename): def lint(filename): + return _new_lint(filename) + + +def _old_lint(filename): findings = [] max_error_level = 0 @@ -429,11 +435,7 @@ def main(input_args=None): help="return non-zero exit code on warnings " "as well as errors", ) args = parser.parse_args(input_args) - # max_error_level = 0 - # for filename in input_files: - # prob_level = lint(filename) - # max_error_level = max(max_error_level, prob_level) input_files = workflow_files(args.input) if len(input_files) > 0: prob_levels = list(map(lint, input_files)) diff --git a/lint-workflow/flake.lock b/lint-workflow/flake.lock new file mode 100644 index 00000000..adbaed05 --- /dev/null +++ b/lint-workflow/flake.lock @@ -0,0 +1,60 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1701680307, + "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", + "type": "github" + }, + "original": { + "id": "flake-utils", + "type": "indirect" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1669833724, + "narHash": "sha256-/HEZNyGbnQecrgJnfE8d0WC5c1xuPSD2LUpB6YXlg4c=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "4d2b37a84fad1091b9de401eb450aae66f1a741e", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "22.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/lint-workflow/flake.nix b/lint-workflow/flake.nix new file mode 100644 index 00000000..f8f65776 --- /dev/null +++ b/lint-workflow/flake.nix @@ -0,0 +1,25 @@ +{ + description = "GitHub Action Linter"; + inputs = { nixpkgs.url = "github:nixos/nixpkgs/22.11"; }; + + outputs = { self, nixpkgs, flake-utils}: + flake-utils.lib.eachDefaultSystem (system: + let + #pkgs = nixpkgs.legacyPackages.x86_64-linux.pkgs; + pkgs = nixpkgs.legacyPackages.${system}; + packageName = "workflow-linter"; + in { + devShells.default = pkgs.mkShell { + name = "${packageName}"; + buildInputs = [ + pkgs.pipenv + pkgs.python311 + ]; + shellHook = '' + echo "Welcome in $name" + export PS1="\[\e[1;33m\][nix(workflow-linter)]\$\[\e[0m\] " + ''; + }; + } + ); +} diff --git a/lint-workflow/settings.py b/lint-workflow/settings.py new file mode 100644 index 00000000..cad8e75f --- /dev/null +++ b/lint-workflow/settings.py @@ -0,0 +1,4 @@ +enabled_rules = [ + 'src.rules.name_exists.RuleNameExists', + 'src.rules.name_capitalized.RuleNameCapitalized' +] diff --git a/lint-workflow/src/load.py b/lint-workflow/src/load.py index 914a43ec..4614ed57 100644 --- a/lint-workflow/src/load.py +++ b/lint-workflow/src/load.py @@ -1,34 +1,75 @@ +import importlib +from typing import List + from ruamel.yaml import YAML from ruamel.yaml.comments import CommentedMap from .models.job import Job from .models.step import Step from .models.workflow import Workflow +from .rule import Rule yaml = YAML() -def load_workflow(filename: str) -> CommentedMap: - with open(filename) as file: - return yaml.load(file) +class WorkflowBuilder: + @classmethod + def __load_workflow(cls, filename: str) -> CommentedMap: + with open(filename) as file: + return yaml.load(file) + + @classmethod + def __build_workflow(cls, loaded_yaml: str) -> Workflow: + return Workflow.from_dict({ + **loaded_yaml, + "jobs": {str(job_key): Job.init(job_key, { + **job, + "steps": [ + Step.init(idx, job_key, step_data) + for idx, step_data in enumerate(job["steps"]) + ] + }) for job_key, job in loaded_yaml["jobs"].items()} + }) + + @classmethod + def build(cls, filename: str) -> Workflow: + return cls.__build_workflow(cls.__load_workflow(filename)) -def build_workflow(loaded_yaml: str) -> Workflow: - return Workflow.from_dict({ - **loaded_yaml, - "jobs": {str(job_key): Job.from_dict({ - **job, - "steps": [Step.from_dict(step) for step in job["steps"]] - }) for job_key, job in loaded_yaml["jobs"].items()} - }) +class Rules: + workflow: List[Rule] = [] + job: List[Rule] = [] + step: List[Rule] = [] + def __init__(self, settings: List[str], verbose: bool = False) -> None: + for rule in settings: + module_name = rule.split('.') + module_name = ".".join(module_name[:-1]) + rule_name = rule.split('.')[-1] -def get_workflow(filename: str) -> Workflow: - return build_workflow(load_workflow(filename)) + try: + rule_module = getattr(importlib.import_module(module_name), rule_name) + rule_inst = rule_module() + if Workflow in rule_inst.compatibility: + self.workflow.append(rule_inst) + if Job in rule_inst.compatibility: + self.job.append(rule_inst) + if Step in rule_inst.compatibility: + self.step.append(rule_inst) + except Error: + print(f"Error loading: {rule}\n{Error}") -""" -workflow = load_workflow("tests/fixtures/test.yml") -workflow["jobs"]["crowdin-pull"]["steps"][0]._yaml_comment # has the comment in it -""" + if verbose: + print("===== Loaded Rules =====") + print("workflow rules:") + for rule in self.workflow: + print(f" - {type(rule).__name__}") + print("job rules:") + for rule in self.job: + print(f" - {type(rule).__name__}") + print("step rules:") + for rule in self.step: + print(f" - {type(rule).__name__}") + print("========================\n") diff --git a/lint-workflow/src/models/job.py b/lint-workflow/src/models/job.py index b8d12622..35b21336 100644 --- a/lint-workflow/src/models/job.py +++ b/lint-workflow/src/models/job.py @@ -1,5 +1,5 @@ from dataclasses import dataclass, field -from typing import List, Optional +from typing import List, Optional, Self from dataclasses_json import config, dataclass_json, Undefined from ruamel.yaml.comments import CommentedMap @@ -10,6 +10,14 @@ @dataclass class Job: runs_on: str = field(metadata=config(field_name="runs-on")) - name: str = None + key: Optional[str] = None + name: Optional[str] = None env: Optional[CommentedMap] = None steps: List[Step] = None + + @classmethod + def init(cls, key: str, data: CommentedMap) -> Self: + new_job = cls.from_dict(data) + new_job.key = key + + return new_job diff --git a/lint-workflow/src/models/step.py b/lint-workflow/src/models/step.py index 6ec5f94c..8fb16165 100644 --- a/lint-workflow/src/models/step.py +++ b/lint-workflow/src/models/step.py @@ -1,14 +1,30 @@ from dataclasses import dataclass, field -from typing import Optional +from typing import Optional, Self -from dataclasses_json import dataclass_json, Undefined +from dataclasses_json import config, dataclass_json, Undefined from ruamel.yaml.comments import CommentedMap @dataclass_json(undefined=Undefined.EXCLUDE) @dataclass class Step: + key: Optional[int] = None + job: Optional[str] = None name: Optional[str] = None env: Optional[CommentedMap] = None uses: Optional[str] = None + uses_comment: Optional[str] = None + uses_with: Optional[CommentedMap] = field(metadata=config(field_name="with"), default=None) run: Optional[str] = None + + @classmethod + def init(cls, idx: int, job: str, data: CommentedMap) -> Self: + new_step = cls.from_dict(data) + + new_step.key = idx + new_step.job = job + + if "uses" in data.ca.items and data.ca.items["uses"][2]: + new_step.uses_comment = data.ca.items["uses"][2].value.replace('\n', '') + + return new_step diff --git a/lint-workflow/src/models/workflow.py b/lint-workflow/src/models/workflow.py index 50beb154..59da2e5f 100644 --- a/lint-workflow/src/models/workflow.py +++ b/lint-workflow/src/models/workflow.py @@ -1,4 +1,5 @@ from dataclasses import dataclass, field +from typing import Optional from dataclasses_json import config, dataclass_json, Undefined from ruamel.yaml.comments import CommentedMap @@ -9,6 +10,7 @@ @dataclass_json(undefined=Undefined.EXCLUDE) @dataclass class Workflow: - name: str = None - on: CommentedMap = None - jobs: CommentedMap = None + key: str = "" + name: Optional[str] = None + on: Optional[CommentedMap] = None + jobs: Optional[CommentedMap] = None diff --git a/lint-workflow/src/rule.py b/lint-workflow/src/rule.py new file mode 100644 index 00000000..993a5b06 --- /dev/null +++ b/lint-workflow/src/rule.py @@ -0,0 +1,46 @@ +from typing import Union, List + +from .models.workflow import Workflow +from .models.job import Job +from .models.step import Step +from .utils import LintFinding + + +class Rule: + message: str = "error" + on_fail: str = "error" + compatibility: List[Union[Workflow, Job, Step]] = [Workflow, Job, Step] + + def fn(self, obj: Union[Workflow, Job, Step]) -> bool: + return False + + def build_lint_message(self, message: str, obj: Union[Workflow, Job, Step]) -> str: + obj_type = type(obj) + + if obj_type == Step: + return f"{obj_type.__name__} [{obj.job}.{obj.key}] => {message}" + elif obj_type == Job: + return f"{obj_type.__name__} [{obj.key}] => {message}" + else: + return f"{obj_type.__name__} => {message}" + + def execute(self, obj: Union[Workflow, Job, Step]) -> Union[LintFinding, None]: + if type(obj) not in self.compatibility: + return LintFinding( + self.build_lint_message( + f"{type(obj).__name__} not compatible with {type(self).__name__}", + obj + ), + "error" + ) + + try: + if self.fn(obj): + return None + except: + return LintFinding( + self.build_lint_message(f"failed to apply {type(self).__name__}", obj), + "error" + ) + + return LintFinding(self.build_lint_message(self.message, obj), self.on_fail) diff --git a/lint-workflow/src/rules/__init__.py b/lint-workflow/src/rules/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lint-workflow/src/rules/name_capitalized.py b/lint-workflow/src/rules/name_capitalized.py new file mode 100644 index 00000000..4df12360 --- /dev/null +++ b/lint-workflow/src/rules/name_capitalized.py @@ -0,0 +1,17 @@ +from typing import Union + +from ..rule import Rule +from ..models.workflow import Workflow +from ..models.job import Job +from ..models.step import Step + + +class RuleNameCapitalized(Rule): + def __init__(self): + self.message = "name must capitalized" + self.on_fail = "error" + + def fn(self, obj: Union[Workflow, Job, Step]): + if obj.name: + return obj.name[0].isupper() + return True # Force passing if obj.name doesn't exist diff --git a/lint-workflow/src/rules/name_exists.py b/lint-workflow/src/rules/name_exists.py new file mode 100644 index 00000000..7d81fd51 --- /dev/null +++ b/lint-workflow/src/rules/name_exists.py @@ -0,0 +1,15 @@ +from typing import Union + +from ..rule import Rule +from ..models.workflow import Workflow +from ..models.job import Job +from ..models.step import Step + + +class RuleNameExists(Rule): + def __init__(self): + self.message = "name must exist" + self.on_fail = "error" + + def fn(self, obj: Union[Workflow, Job, Step]): + return obj.name is not None diff --git a/lint-workflow/src/utils.py b/lint-workflow/src/utils.py new file mode 100644 index 00000000..800169eb --- /dev/null +++ b/lint-workflow/src/utils.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + + +@dataclass +class LintFinding: + """Represents a linting problem.""" + description: str = "" + level: str = None diff --git a/lint-workflow/tests/OLD_test_action_update.py b/lint-workflow/tests/OLD_test_action_update.py new file mode 100644 index 00000000..b2c12e6f --- /dev/null +++ b/lint-workflow/tests/OLD_test_action_update.py @@ -0,0 +1,20 @@ +import pytest +import urllib3 as urllib + +from lint import get_action_update, memoized_action_update_urls + +http = urllib.PoolManager() + + +@pytest.mark.skip() +def test_action_update(): + action_id = "actions/checkout@86f86b36ef15e6570752e7175f451a512eac206b" + sub_string = "github.com" + update_url = get_action_update(action_id) + assert str(sub_string) in str(update_url) + + r = http.request("GET", update_url) + + assert r.status == 200 + + assert "actions/checkout" in memoized_action_update_urls diff --git a/lint-workflow/tests/OLD_test_lint.py b/lint-workflow/tests/OLD_test_lint.py new file mode 100644 index 00000000..4f89a084 --- /dev/null +++ b/lint-workflow/tests/OLD_test_lint.py @@ -0,0 +1,23 @@ +import pytest + +from lint import lint +from .configs import FIXTURES_DIR + + +@pytest.mark.skip() +def test_lint(capfd): + file_path = f"{FIXTURES_DIR}/test.yml" + lint_output = lint(file_path) + out, err = capfd.readouterr() + assert ( + "\x1b[33mwarning\x1b[0m Name value for workflow is not capitalized. [crowdin Pull]" + in out + ) + assert ( + "\x1b[33mwarning\x1b[0m Step 4 of job key 'crowdin-pull' uses an outdated action, consider updating it" + in out + ) + assert ( + "\x1b[31merror\x1b[0m Step 2 of job key 'crowdin-pull' uses an non-existing action: Azure/logi@77f1b2e3fb80c0e8645114159d17008b8a2e475a." + in out + ) diff --git a/lint-workflow/tests/OLD_test_main.py b/lint-workflow/tests/OLD_test_main.py new file mode 100644 index 00000000..bc4f863a --- /dev/null +++ b/lint-workflow/tests/OLD_test_main.py @@ -0,0 +1,66 @@ +import pytest + +from lint import main +from .configs import FIXTURES_DIR + +# Tests for argparse inputs and outputs using capsys.readouterr() + +FIXTURES_DIR = "./tests/fixtures" + + +@pytest.mark.skip() +def test_main_single_file(capsys): + main([f"{FIXTURES_DIR}/test.yml"]) + captured = capsys.readouterr() + result = captured.out + assert "test.yml" in result + + +@pytest.mark.skip() +def test_main_multiple_files(capsys): + main([f"{FIXTURES_DIR}/test.yml {FIXTURES_DIR}/test-alt.yml"]) + captured = capsys.readouterr() + result = captured.out + assert isinstance(result, str) + assert "test.yml" in result + assert "test-alt.yml" in result + + +@pytest.mark.skip() +def test_main_folder(capsys): + main([f"{FIXTURES_DIR}"]) + captured = capsys.readouterr() + result = captured.out + assert isinstance(result, str) + assert "test.yml" in result + assert "test-alt.yml" in result + + +@pytest.mark.skip() +def test_main_folder_and_files(capsys): + main([f"{FIXTURES_DIR}/test.yml {FIXTURES_DIR}"]) + captured = capsys.readouterr() + result = captured.out + print(result) + + +@pytest.mark.skip() +def test_main_not_found(capsys): + # File that doesn't exist + main(["not-a-real-file.yml"]) + captured = capsys.readouterr() + result = captured.out + assert isinstance(result, str) + assert 'File(s)/Directory: "not-a-real-file.yml" does not exist, exiting.' in result + # Empty string + main([""]) + captured = capsys.readouterr() + result = captured.out + assert isinstance(result, str) + assert 'File(s)/Directory: "" does not exist, exiting.' in result + # Spaces in string + main([" "]) + captured = capsys.readouterr() + result = captured.out + assert isinstance(result, str) + assert 'File(s)/Directory: " " does not exist, exiting.' in result diff --git a/lint-workflow/tests/OLD_test_workflow_files.py b/lint-workflow/tests/OLD_test_workflow_files.py new file mode 100644 index 00000000..6d969306 --- /dev/null +++ b/lint-workflow/tests/OLD_test_workflow_files.py @@ -0,0 +1,37 @@ +import os +import pytest + +from lint import workflow_files +from .configs import FIXTURES_DIR + + +@pytest.mark.skip() +def test_workflow_files(): + assert workflow_files("") == [] + assert workflow_files("not-a-real-file.yml") == [] + assert workflow_files(f"{FIXTURES_DIR}/test.yml") == [f"{FIXTURES_DIR}/test.yml"] + # multiple files + assert workflow_files( + f"{FIXTURES_DIR}/test.yml {FIXTURES_DIR}/test-alt.yml" + ) == sorted([f"{FIXTURES_DIR}/test.yml", f"{FIXTURES_DIR}/test-alt.yml"]) + # directory + assert workflow_files(FIXTURES_DIR) == sorted( + set( + [ + f"{FIXTURES_DIR}/{file}" + for file in os.listdir(FIXTURES_DIR) + if file.endswith((".yml", ".yaml")) + ] + ) + ) + # directory and files + assert workflow_files(f"{FIXTURES_DIR}/test.yml {FIXTURES_DIR}") == sorted( + set( + [f"{FIXTURES_DIR}/test.yml"] + + [ + f"{FIXTURES_DIR}/{file}" + for file in os.listdir(FIXTURES_DIR) + if file.endswith((".yml", ".yaml")) + ] + ) + ) diff --git a/lint-workflow/tests/fixtures/test-min-incorrect.yaml b/lint-workflow/tests/fixtures/test-min-incorrect.yaml new file mode 100644 index 00000000..5ef34058 --- /dev/null +++ b/lint-workflow/tests/fixtures/test-min-incorrect.yaml @@ -0,0 +1,9 @@ +--- +on: + workflow_dispatch: + +jobs: + job-key: + runs-on: ubuntu-latest + steps: + - run: echo test diff --git a/lint-workflow/tests/fixtures/test-min.yaml b/lint-workflow/tests/fixtures/test-min.yaml index 958883f4..a641f914 100644 --- a/lint-workflow/tests/fixtures/test-min.yaml +++ b/lint-workflow/tests/fixtures/test-min.yaml @@ -9,4 +9,5 @@ jobs: name: Test runs-on: ubuntu-latest steps: - - run: echo test + - name: Test + run: echo test diff --git a/lint-workflow/tests/fixtures/test.yml b/lint-workflow/tests/fixtures/test.yml index 910a2e3e..cd6b71a2 100644 --- a/lint-workflow/tests/fixtures/test.yml +++ b/lint-workflow/tests/fixtures/test.yml @@ -36,8 +36,7 @@ jobs: echo "$i=$VALUE" >> $GITHUB_OUTPUT done - - name: Download translations - uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea # v1.3.2 + - uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea # v1.3.2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} diff --git a/lint-workflow/tests/rules/__init__.py b/lint-workflow/tests/rules/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lint-workflow/tests/rules/test_name_capitalized.py b/lint-workflow/tests/rules/test_name_capitalized.py new file mode 100644 index 00000000..c5ca64b7 --- /dev/null +++ b/lint-workflow/tests/rules/test_name_capitalized.py @@ -0,0 +1,35 @@ +import pytest + +from ..conftest import FIXTURE_DIR +from ..context import src + +from src.load import WorkflowBuilder +from src.rules.name_capitalized import RuleNameCapitalized + + +@pytest.fixture +def correct_workflow(): + return WorkflowBuilder.build(f"{FIXTURE_DIR}/test-min.yaml") + + +@pytest.fixture +def incorrect_workflow(): + return WorkflowBuilder.build(f"{FIXTURE_DIR}/test-min-incorrect.yaml") + + +@pytest.fixture +def rule(): + return RuleNameCapitalized() + + +def test_rule_on_correct_workflow(rule, correct_workflow): + assert rule.fn(correct_workflow) == True + assert rule.fn(correct_workflow.jobs['job-key']) == True + assert rule.fn(correct_workflow.jobs['job-key'].steps[0]) == True + + +def test_rule_on_incorrect_workflow(rule, incorrect_workflow): + print(f"Workflow name: {incorrect_workflow.name}") + assert rule.fn(incorrect_workflow) == False + assert rule.fn(incorrect_workflow.jobs['job-key']) == False + assert rule.fn(incorrect_workflow.jobs['job-key'].steps[0]) == False diff --git a/lint-workflow/tests/rules/test_name_exists.py b/lint-workflow/tests/rules/test_name_exists.py new file mode 100644 index 00000000..c8631bb8 --- /dev/null +++ b/lint-workflow/tests/rules/test_name_exists.py @@ -0,0 +1,35 @@ +import pytest + +from ..conftest import FIXTURE_DIR +from ..context import src + +from src.load import WorkflowBuilder +from src.rules.name_exists import RuleNameExists + + +@pytest.fixture +def correct_workflow(): + return WorkflowBuilder.build(f"{FIXTURE_DIR}/test-min.yaml") + + +@pytest.fixture +def incorrect_workflow(): + return WorkflowBuilder.build(f"{FIXTURE_DIR}/test-min-incorrect.yaml") + + +@pytest.fixture +def rule(): + return RuleNameExists() + + +def test_rule_on_correct_workflow(rule, correct_workflow): + assert rule.fn(correct_workflow) == True + assert rule.fn(correct_workflow.jobs['job-key']) == True + assert rule.fn(correct_workflow.jobs['job-key'].steps[0]) == True + + +def test_rule_on_incorrect_workflow(rule, incorrect_workflow): + print(f"Workflow name: {incorrect_workflow.name}") + assert rule.fn(incorrect_workflow) == False + assert rule.fn(incorrect_workflow.jobs['job-key']) == False + assert rule.fn(incorrect_workflow.jobs['job-key'].steps[0]) == False diff --git a/lint-workflow/tests/test_job.py b/lint-workflow/tests/test_job.py index e505bb94..4789c746 100644 --- a/lint-workflow/tests/test_job.py +++ b/lint-workflow/tests/test_job.py @@ -5,7 +5,7 @@ @pytest.fixture -def job_default_data(): +def default_job_data(): return { "name": "Test", "runs-on": "ubuntu-latest", @@ -13,19 +13,23 @@ def job_default_data(): } -def test_job_default(job_default_data): - job = src.models.Job.from_dict(job_default_data) +@pytest.fixture +def default_job(default_job_data): + return src.models.Job.init('default-job', default_job_data) + - assert job.name == "Test" - assert job.runs_on == "ubuntu-latest" - assert job.env == None - assert len(job.steps) == 1 +def test_job_default(default_job): + assert default_job.key == "default-job" + assert default_job.name == "Test" + assert default_job.runs_on == "ubuntu-latest" + assert default_job.env == None + assert len(default_job.steps) == 1 -def test_job_extra_kwargs(job_default_data): - job = src.models.Job.from_dict({ +def test_job_extra_kwargs(default_job_data): + job = src.models.Job.init('test-job', { "extra": "test", - **job_default_data + **default_job_data }) with pytest.raises(Exception) as e_info: diff --git a/lint-workflow/tests/test_load.py b/lint-workflow/tests/test_load.py index 7ac54171..f64b4016 100644 --- a/lint-workflow/tests/test_load.py +++ b/lint-workflow/tests/test_load.py @@ -1,6 +1,8 @@ import json import pytest +from ruamel.yaml.comments import CommentedMap + from .conftest import FIXTURE_DIR from .context import src @@ -11,18 +13,5 @@ def workflow_filename(): def test_load_workflow(workflow_filename): - loaded_yaml = src.load.load_workflow(workflow_filename) - print(loaded_yaml) - print(type(loaded_yaml)) - print(dir(loaded_yaml)) - - assert False - - -def test_build_workflow(workflow_filename): - workflow = src.load.build_workflow( - src.load.load_workflow(workflow_filename) - ) - - assert workflow.name == "crowdin Pull" - assert len(workflow.jobs["crowdin-pull"].steps) == 4 + workflow = src.load.WorkflowBuilder.build(workflow_filename) + assert type(workflow) == src.models.Workflow diff --git a/lint-workflow/tests/test_rule.py b/lint-workflow/tests/test_rule.py new file mode 100644 index 00000000..59accc1d --- /dev/null +++ b/lint-workflow/tests/test_rule.py @@ -0,0 +1,82 @@ +import pytest +from typing import Union + +from ruamel.yaml import YAML + +from .conftest import FIXTURE_DIR +from .context import src + +from src.load import WorkflowBuilder +from src.rule import Rule +from src.models import Workflow, Job, Step + + +class RuleStep(Rule): + def __init__(self): + self.message = "test" + self.on_fail = "error" + self.compatibility = [Step] + + +class RuleNameExists(Rule): + def __init__(self): + self.message = "name must exist" + self.on_fail = "error" + + def fn(self, obj: Union[Workflow, Job, Step]) -> bool: + print(f"{type(self).__name__}\n{obj}") + return obj.name is not None + + +@pytest.fixture +def step_rule(): + return RuleStep() + + +@pytest.fixture +def exists_rule(): + return RuleNameExists() + + +@pytest.fixture +def correct_workflow(): + return WorkflowBuilder.build(f"{FIXTURE_DIR}/test-min.yaml") + + +@pytest.fixture +def incorrect_workflow(): + return WorkflowBuilder.build(f"{FIXTURE_DIR}/test-min-incorrect.yaml") + + +def test_build_lint_message(step_rule, correct_workflow): + assert step_rule.build_lint_message( + "test", correct_workflow + ) == "Workflow => test" + + assert step_rule.build_lint_message( + "test", correct_workflow.jobs['job-key'] + ) == "Job [job-key] => test" + + assert step_rule.build_lint_message( + "test", correct_workflow.jobs['job-key'].steps[0] + ) == "Step [job-key.0] => test" + + +def test_rule_compatibility(step_rule, correct_workflow): + assert "not compatible" in step_rule.execute(correct_workflow).description + assert "not compatible" in step_rule.execute(correct_workflow.jobs['job-key']).description + assert "not compatible" not in step_rule.execute( + correct_workflow.jobs['job-key'].steps[0] + ).description + + +def test_correct_rule_execution(exists_rule, correct_workflow): + assert exists_rule.execute(correct_workflow) == None + assert exists_rule.execute(correct_workflow.jobs['job-key']) == None + assert exists_rule.execute(correct_workflow.jobs['job-key'].steps[0]) == None + + +def test_incorrect_rule_execution(exists_rule, incorrect_workflow): + assert "name must exist" in exists_rule.execute(incorrect_workflow).description + assert "name must exist" in exists_rule.execute(incorrect_workflow.jobs['job-key']).description + assert "name must exist" in exists_rule.execute(incorrect_workflow.jobs['job-key'].steps[0]).description diff --git a/lint-workflow/tests/test_step.py b/lint-workflow/tests/test_step.py index c417d67b..9a69cad2 100644 --- a/lint-workflow/tests/test_step.py +++ b/lint-workflow/tests/test_step.py @@ -1,48 +1,77 @@ import json import pytest +from ruamel.yaml import YAML + from .context import src @pytest.fixture -def step_default_data(): - return { - "run": "echo \"test\"" - } +def default_step(): + step_str = """\ +name: Default Step +run: echo "test" +""" + yaml = YAML() + step_yaml = yaml.load(step_str) + return src.models.Step.init(0, "default", step_yaml) -def test_step_default(step_default_data): - step = src.models.Step.from_dict(step_default_data) - assert step.name == None - assert step.env == None - assert step.uses == None - assert step.run == "echo \"test\"" +@pytest.fixture +def uses_step(): + step_str = """\ +name: Download Artifacts +uses: bitwarden/download-artifacts@main # v1.0.0 +with: + workflow: upload-test-artifacts.yml + artifacts: artifact + path: artifact + branch: main +""" + yaml = YAML() + step_yaml = yaml.load(step_str) + return src.models.Step.init(0, "default", step_yaml) -@pytest.mark.skip() -def test_step_keyword_field(step_default_data): - data = { - "with": {"config": "test.json"}, - **step_default_data - } - step = src.models.Step.from_dict(data) - assert "with_field" not in step.json(by_alias=True) - assert json.loads(step.json(by_alias=True))["with"] == {"config": "test.json"} +def test_step_default(default_step): + assert default_step.key == 0 + assert default_step.job == "default" + assert default_step.name == "Default Step" + assert default_step.env == None + assert default_step.uses == None + assert default_step.uses_with == None + assert default_step.run == "echo \"test\"" -@pytest.mark.skip() -def test_step_no_keyword_field(step_default_data): - step = src.models.Step.from_dict(step_default_data) - assert step.with_field == None - assert "with_field" not in step.json(by_alias=True) +def test_step_no_keyword_field(default_step): + assert default_step.uses_with == None + assert "uses_with" not in default_step.to_json() -def test_step_extra_kwargs(step_default_data): - step = src.models.Step.from_dict({ - "name": "test step", - "extra": "test", - **step_default_data - }) +def test_step_extra_kwargs(default_step): with pytest.raises(Exception) as e_info: - assert step.extra == "test" + assert default_step.extra == "test" + + +def test_step_keyword_field(uses_step): + expected_response = { + "workflow": "upload-test-artifacts.yml", + "artifacts": "artifact", + "path": "artifact", + "branch": "main" + } + + step_json = uses_step.to_json() + assert uses_step.key == 0 + assert "uses_with" not in step_json + assert "with" in step_json + assert json.loads(uses_step.to_json())["with"] == expected_response + + + +def test_step_comment(uses_step): + assert uses_step.key == 0 + assert uses_step.job == "default" + assert uses_step.uses_comment is not None + assert uses_step.uses_comment == "# v1.0.0" From 42ad11aee703a238c77134bba25dfe0046338ed2 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 29 Dec 2023 15:35:57 -0800 Subject: [PATCH 08/57] update pip lockfile --- lint-workflow/Pipfile.lock | 416 +++++++++++++++++++++++++++++++++++++ 1 file changed, 416 insertions(+) create mode 100644 lint-workflow/Pipfile.lock diff --git a/lint-workflow/Pipfile.lock b/lint-workflow/Pipfile.lock new file mode 100644 index 00000000..7889f32b --- /dev/null +++ b/lint-workflow/Pipfile.lock @@ -0,0 +1,416 @@ +{ + "_meta": { + "hash": { + "sha256": "889524a1d31670cc531521bb7ce622380b376403b3c556248a5df8be064fbb64" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.11" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "annotated-types": { + "hashes": [ + "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43", + "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d" + ], + "markers": "python_version >= '3.8'", + "version": "==0.6.0" + }, + "dataclasses-json": { + "hashes": [ + "sha256:35cb40aae824736fdf959801356641836365219cfe14caeb115c39136f775d2a", + "sha256:4aeb343357997396f6bca1acae64e486c3a723d8f5c76301888abeccf0c45176" + ], + "index": "pypi", + "version": "==0.6.3" + }, + "marshmallow": { + "hashes": [ + "sha256:5d2371bbe42000f2b3fb5eaa065224df7d8f8597bc19a1bbfa5bfe7fba8da889", + "sha256:684939db93e80ad3561392f47be0230743131560a41c5110684c16e21ade0a5c" + ], + "markers": "python_version >= '3.8'", + "version": "==3.20.1" + }, + "mypy-extensions": { + "hashes": [ + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.0" + }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2" + }, + "pydantic": { + "hashes": [ + "sha256:b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a", + "sha256:d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4" + ], + "index": "pypi", + "version": "==2.5.3" + }, + "pydantic-core": { + "hashes": [ + "sha256:00646784f6cd993b1e1c0e7b0fdcbccc375d539db95555477771c27555e3c556", + "sha256:00b1087dabcee0b0ffd104f9f53d7d3eaddfaa314cdd6726143af6bc713aa27e", + "sha256:0348b1dc6b76041516e8a854ff95b21c55f5a411c3297d2ca52f5528e49d8411", + "sha256:036137b5ad0cb0004c75b579445a1efccd072387a36c7f217bb8efd1afbe5245", + "sha256:095b707bb287bfd534044166ab767bec70a9bba3175dcdc3371782175c14e43c", + "sha256:0c08de15d50fa190d577e8591f0329a643eeaed696d7771760295998aca6bc66", + "sha256:1302a54f87b5cd8528e4d6d1bf2133b6aa7c6122ff8e9dc5220fbc1e07bffebd", + "sha256:172de779e2a153d36ee690dbc49c6db568d7b33b18dc56b69a7514aecbcf380d", + "sha256:1b027c86c66b8627eb90e57aee1f526df77dc6d8b354ec498be9a757d513b92b", + "sha256:1ce830e480f6774608dedfd4a90c42aac4a7af0a711f1b52f807130c2e434c06", + "sha256:1fd0c1d395372843fba13a51c28e3bb9d59bd7aebfeb17358ffaaa1e4dbbe948", + "sha256:23598acb8ccaa3d1d875ef3b35cb6376535095e9405d91a3d57a8c7db5d29341", + "sha256:24368e31be2c88bd69340fbfe741b405302993242ccb476c5c3ff48aeee1afe0", + "sha256:26a92ae76f75d1915806b77cf459811e772d8f71fd1e4339c99750f0e7f6324f", + "sha256:27e524624eace5c59af499cd97dc18bb201dc6a7a2da24bfc66ef151c69a5f2a", + "sha256:2b8719037e570639e6b665a4050add43134d80b687288ba3ade18b22bbb29dd2", + "sha256:2c5bcf3414367e29f83fd66f7de64509a8fd2368b1edf4351e862910727d3e51", + "sha256:2dbe357bc4ddda078f79d2a36fc1dd0494a7f2fad83a0a684465b6f24b46fe80", + "sha256:2f5fa187bde8524b1e37ba894db13aadd64faa884657473b03a019f625cee9a8", + "sha256:2f6ffc6701a0eb28648c845f4945a194dc7ab3c651f535b81793251e1185ac3d", + "sha256:314ccc4264ce7d854941231cf71b592e30d8d368a71e50197c905874feacc8a8", + "sha256:36026d8f99c58d7044413e1b819a67ca0e0b8ebe0f25e775e6c3d1fabb3c38fb", + "sha256:36099c69f6b14fc2c49d7996cbf4f87ec4f0e66d1c74aa05228583225a07b590", + "sha256:36fa402dcdc8ea7f1b0ddcf0df4254cc6b2e08f8cd80e7010d4c4ae6e86b2a87", + "sha256:370ffecb5316ed23b667d99ce4debe53ea664b99cc37bfa2af47bc769056d534", + "sha256:3860c62057acd95cc84044e758e47b18dcd8871a328ebc8ccdefd18b0d26a21b", + "sha256:399ac0891c284fa8eb998bcfa323f2234858f5d2efca3950ae58c8f88830f145", + "sha256:3a0b5db001b98e1c649dd55afa928e75aa4087e587b9524a4992316fa23c9fba", + "sha256:3dcf1978be02153c6a31692d4fbcc2a3f1db9da36039ead23173bc256ee3b91b", + "sha256:4241204e4b36ab5ae466ecec5c4c16527a054c69f99bba20f6f75232a6a534e2", + "sha256:438027a975cc213a47c5d70672e0d29776082155cfae540c4e225716586be75e", + "sha256:43e166ad47ba900f2542a80d83f9fc65fe99eb63ceec4debec160ae729824052", + "sha256:478e9e7b360dfec451daafe286998d4a1eeaecf6d69c427b834ae771cad4b622", + "sha256:4ce8299b481bcb68e5c82002b96e411796b844d72b3e92a3fbedfe8e19813eab", + "sha256:4f86f1f318e56f5cbb282fe61eb84767aee743ebe32c7c0834690ebea50c0a6b", + "sha256:55a23dcd98c858c0db44fc5c04fc7ed81c4b4d33c653a7c45ddaebf6563a2f66", + "sha256:599c87d79cab2a6a2a9df4aefe0455e61e7d2aeede2f8577c1b7c0aec643ee8e", + "sha256:5aa90562bc079c6c290f0512b21768967f9968e4cfea84ea4ff5af5d917016e4", + "sha256:64634ccf9d671c6be242a664a33c4acf12882670b09b3f163cd00a24cffbd74e", + "sha256:667aa2eac9cd0700af1ddb38b7b1ef246d8cf94c85637cbb03d7757ca4c3fdec", + "sha256:6a31d98c0d69776c2576dda4b77b8e0c69ad08e8b539c25c7d0ca0dc19a50d6c", + "sha256:6af4b3f52cc65f8a0bc8b1cd9676f8c21ef3e9132f21fed250f6958bd7223bed", + "sha256:6c8edaea3089bf908dd27da8f5d9e395c5b4dc092dbcce9b65e7156099b4b937", + "sha256:71d72ca5eaaa8d38c8df16b7deb1a2da4f650c41b58bb142f3fb75d5ad4a611f", + "sha256:72f9a942d739f09cd42fffe5dc759928217649f070056f03c70df14f5770acf9", + "sha256:747265448cb57a9f37572a488a57d873fd96bf51e5bb7edb52cfb37124516da4", + "sha256:75ec284328b60a4e91010c1acade0c30584f28a1f345bc8f72fe8b9e46ec6a96", + "sha256:78d0768ee59baa3de0f4adac9e3748b4b1fffc52143caebddfd5ea2961595277", + "sha256:78ee52ecc088c61cce32b2d30a826f929e1708f7b9247dc3b921aec367dc1b23", + "sha256:7be719e4d2ae6c314f72844ba9d69e38dff342bc360379f7c8537c48e23034b7", + "sha256:7e1f4744eea1501404b20b0ac059ff7e3f96a97d3e3f48ce27a139e053bb370b", + "sha256:7e90d6cc4aad2cc1f5e16ed56e46cebf4877c62403a311af20459c15da76fd91", + "sha256:7ebe3416785f65c28f4f9441e916bfc8a54179c8dea73c23023f7086fa601c5d", + "sha256:7f41533d7e3cf9520065f610b41ac1c76bc2161415955fbcead4981b22c7611e", + "sha256:7f5025db12fc6de7bc1104d826d5aee1d172f9ba6ca936bf6474c2148ac336c1", + "sha256:86c963186ca5e50d5c8287b1d1c9d3f8f024cbe343d048c5bd282aec2d8641f2", + "sha256:86ce5fcfc3accf3a07a729779d0b86c5d0309a4764c897d86c11089be61da160", + "sha256:8a14c192c1d724c3acbfb3f10a958c55a2638391319ce8078cb36c02283959b9", + "sha256:8b93785eadaef932e4fe9c6e12ba67beb1b3f1e5495631419c784ab87e975670", + "sha256:8ed1af8692bd8d2a29d702f1a2e6065416d76897d726e45a1775b1444f5928a7", + "sha256:92879bce89f91f4b2416eba4429c7b5ca22c45ef4a499c39f0c5c69257522c7c", + "sha256:94fc0e6621e07d1e91c44e016cc0b189b48db053061cc22d6298a611de8071bb", + "sha256:982487f8931067a32e72d40ab6b47b1628a9c5d344be7f1a4e668fb462d2da42", + "sha256:9862bf828112e19685b76ca499b379338fd4c5c269d897e218b2ae8fcb80139d", + "sha256:99b14dbea2fdb563d8b5a57c9badfcd72083f6006caf8e126b491519c7d64ca8", + "sha256:9c6a5c79b28003543db3ba67d1df336f253a87d3112dac3a51b94f7d48e4c0e1", + "sha256:a19b794f8fe6569472ff77602437ec4430f9b2b9ec7a1105cfd2232f9ba355e6", + "sha256:a306cdd2ad3a7d795d8e617a58c3a2ed0f76c8496fb7621b6cd514eb1532cae8", + "sha256:a3dde6cac75e0b0902778978d3b1646ca9f438654395a362cb21d9ad34b24acf", + "sha256:a874f21f87c485310944b2b2734cd6d318765bcbb7515eead33af9641816506e", + "sha256:a983cca5ed1dd9a35e9e42ebf9f278d344603bfcb174ff99a5815f953925140a", + "sha256:aca48506a9c20f68ee61c87f2008f81f8ee99f8d7f0104bff3c47e2d148f89d9", + "sha256:b2602177668f89b38b9f84b7b3435d0a72511ddef45dc14446811759b82235a1", + "sha256:b3e5fe4538001bb82e2295b8d2a39356a84694c97cb73a566dc36328b9f83b40", + "sha256:b6ca36c12a5120bad343eef193cc0122928c5c7466121da7c20f41160ba00ba2", + "sha256:b89f4477d915ea43b4ceea6756f63f0288941b6443a2b28c69004fe07fde0d0d", + "sha256:b9a9d92f10772d2a181b5ca339dee066ab7d1c9a34ae2421b2a52556e719756f", + "sha256:c99462ffc538717b3e60151dfaf91125f637e801f5ab008f81c402f1dff0cd0f", + "sha256:cb92f9061657287eded380d7dc455bbf115430b3aa4741bdc662d02977e7d0af", + "sha256:cdee837710ef6b56ebd20245b83799fce40b265b3b406e51e8ccc5b85b9099b7", + "sha256:cf10b7d58ae4a1f07fccbf4a0a956d705356fea05fb4c70608bb6fa81d103cda", + "sha256:d15687d7d7f40333bd8266f3814c591c2e2cd263fa2116e314f60d82086e353a", + "sha256:d5c28525c19f5bb1e09511669bb57353d22b94cf8b65f3a8d141c389a55dec95", + "sha256:d5f916acf8afbcab6bacbb376ba7dc61f845367901ecd5e328fc4d4aef2fcab0", + "sha256:dab03ed811ed1c71d700ed08bde8431cf429bbe59e423394f0f4055f1ca0ea60", + "sha256:db453f2da3f59a348f514cfbfeb042393b68720787bbef2b4c6068ea362c8149", + "sha256:de2a0645a923ba57c5527497daf8ec5df69c6eadf869e9cd46e86349146e5975", + "sha256:dea7fcd62915fb150cdc373212141a30037e11b761fbced340e9db3379b892d4", + "sha256:dfcbebdb3c4b6f739a91769aea5ed615023f3c88cb70df812849aef634c25fbe", + "sha256:dfcebb950aa7e667ec226a442722134539e77c575f6cfaa423f24371bb8d2e94", + "sha256:e0641b506486f0b4cd1500a2a65740243e8670a2549bb02bc4556a83af84ae03", + "sha256:e33b0834f1cf779aa839975f9d8755a7c2420510c0fa1e9fa0497de77cd35d2c", + "sha256:e4ace1e220b078c8e48e82c081e35002038657e4b37d403ce940fa679e57113b", + "sha256:e4cf2d5829f6963a5483ec01578ee76d329eb5caf330ecd05b3edd697e7d768a", + "sha256:e574de99d735b3fc8364cba9912c2bec2da78775eba95cbb225ef7dda6acea24", + "sha256:e646c0e282e960345314f42f2cea5e0b5f56938c093541ea6dbf11aec2862391", + "sha256:e8a5ac97ea521d7bde7621d86c30e86b798cdecd985723c4ed737a2aa9e77d0c", + "sha256:eedf97be7bc3dbc8addcef4142f4b4164066df0c6f36397ae4aaed3eb187d8ab", + "sha256:ef633add81832f4b56d3b4c9408b43d530dfca29e68fb1b797dcb861a2c734cd", + "sha256:f27207e8ca3e5e021e2402ba942e5b4c629718e665c81b8b306f3c8b1ddbb786", + "sha256:f85f3843bdb1fe80e8c206fe6eed7a1caeae897e496542cee499c374a85c6e08", + "sha256:f8e81e4b55930e5ffab4a68db1af431629cf2e4066dbdbfef65348b8ab804ea8", + "sha256:f96ae96a060a8072ceff4cfde89d261837b4294a4f28b84a28765470d502ccc6", + "sha256:fd9e98b408384989ea4ab60206b8e100d8687da18b5c813c11e92fd8212a98e0", + "sha256:ffff855100bc066ff2cd3aa4a60bc9534661816b110f0243e59503ec2df38421" + ], + "markers": "python_version >= '3.7'", + "version": "==2.14.6" + }, + "pyyaml": { + "hashes": [ + "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", + "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", + "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", + "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", + "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", + "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", + "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", + "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", + "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", + "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", + "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", + "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", + "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", + "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", + "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", + "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", + "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", + "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", + "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", + "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", + "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", + "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", + "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", + "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", + "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", + "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", + "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", + "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", + "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", + "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", + "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", + "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", + "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", + "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", + "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", + "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", + "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", + "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", + "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", + "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", + "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", + "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", + "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", + "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", + "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", + "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", + "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", + "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", + "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", + "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" + ], + "index": "pypi", + "version": "==6.0.1" + }, + "ruamel.yaml": { + "hashes": [ + "sha256:61917e3a35a569c1133a8f772e1226961bf5a1198bea7e23f06a0841dea1ab0e", + "sha256:a013ac02f99a69cdd6277d9664689eb1acba07069f912823177c5eced21a6ada" + ], + "index": "pypi", + "version": "==0.18.5" + }, + "ruamel.yaml.clib": { + "hashes": [ + "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d", + "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001", + "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462", + "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9", + "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe", + "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b", + "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b", + "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615", + "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62", + "sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15", + "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b", + "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1", + "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9", + "sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675", + "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899", + "sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7", + "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7", + "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312", + "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa", + "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91", + "sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b", + "sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6", + "sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3", + "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334", + "sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5", + "sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3", + "sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe", + "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c", + "sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed", + "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337", + "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880", + "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f", + "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d", + "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248", + "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d", + "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf", + "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512", + "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069", + "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb", + "sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942", + "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d", + "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31", + "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92", + "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5", + "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28", + "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d", + "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1", + "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2", + "sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875", + "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412" + ], + "markers": "platform_python_implementation == 'CPython' and python_version < '3.13'", + "version": "==0.2.8" + }, + "typing-extensions": { + "hashes": [ + "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", + "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + ], + "markers": "python_version >= '3.8'", + "version": "==4.9.0" + }, + "typing-inspect": { + "hashes": [ + "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", + "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78" + ], + "version": "==0.9.0" + }, + "urllib3": { + "hashes": [ + "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", + "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54" + ], + "index": "pypi", + "version": "==2.1.0" + } + }, + "develop": { + "black": { + "hashes": [ + "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50", + "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f", + "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e", + "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec", + "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055", + "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3", + "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5", + "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54", + "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b", + "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e", + "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e", + "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba", + "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea", + "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59", + "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d", + "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0", + "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9", + "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a", + "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e", + "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba", + "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2", + "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2" + ], + "index": "pypi", + "version": "==23.12.1" + }, + "click": { + "hashes": [ + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.7" + }, + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "mypy-extensions": { + "hashes": [ + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.0" + }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2" + }, + "pathspec": { + "hashes": [ + "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", + "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" + ], + "markers": "python_version >= '3.8'", + "version": "==0.12.1" + }, + "platformdirs": { + "hashes": [ + "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380", + "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420" + ], + "markers": "python_version >= '3.8'", + "version": "==4.1.0" + }, + "pluggy": { + "hashes": [ + "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12", + "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7" + ], + "markers": "python_version >= '3.8'", + "version": "==1.3.0" + }, + "pytest": { + "hashes": [ + "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac", + "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5" + ], + "index": "pypi", + "version": "==7.4.3" + } + } +} From 34a431945daf69c1d47fdedb424f08c9ccff2fcb Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Wed, 3 Jan 2024 15:16:14 -0800 Subject: [PATCH 09/57] Build out the majority of the rules --- lint-workflow/Pipfile.lock | 2 +- lint-workflow/README.md | 16 ++-- lint-workflow/actions.json | 54 +++++++++++ lint-workflow/cli.py | 25 +++-- lint-workflow/lint.py | 66 +++++-------- lint-workflow/settings.py | 63 +++++++++++- lint-workflow/src/load.py | 62 ++++++++---- lint-workflow/src/models/job.py | 1 + lint-workflow/src/models/step.py | 12 ++- lint-workflow/src/models/workflow.py | 2 +- lint-workflow/src/rule.py | 25 +++-- .../src/rules/job_environment_prefix.py | 23 +++++ lint-workflow/src/rules/name_capitalized.py | 4 +- lint-workflow/src/rules/name_exists.py | 4 +- lint-workflow/src/rules/pinned_job_runner.py | 14 +++ lint-workflow/src/rules/step_approved.py | 56 +++++++++++ lint-workflow/src/rules/step_pinned.py | 42 ++++++++ lint-workflow/src/utils.py | 23 +++++ lint-workflow/tests/conftest.py | 2 +- lint-workflow/tests/context.py | 3 +- lint-workflow/tests/fixtures/test_a.yaml | 2 +- .../rules/test_job_environment_prefix.py | 64 +++++++++++++ .../tests/rules/test_name_capitalized.py | 89 +++++++++++++++-- lint-workflow/tests/rules/test_name_exists.py | 22 +++-- .../tests/rules/test_pinned_job_runner.py | 58 +++++++++++ .../tests/rules/test_step_approved.py | 92 ++++++++++++++++++ lint-workflow/tests/rules/test_step_pinned.py | 86 +++++++++++++++++ lint-workflow/tests/test_job.py | 9 +- lint-workflow/tests/test_load.py | 32 ++++++- lint-workflow/tests/test_rule.py | 95 +++++++++++++------ lint-workflow/tests/test_step.py | 5 +- lint-workflow/tests/test_utils.py | 8 ++ lint-workflow/tests/test_workflow.py | 19 ++-- 33 files changed, 919 insertions(+), 161 deletions(-) create mode 100644 lint-workflow/actions.json create mode 100644 lint-workflow/src/rules/job_environment_prefix.py create mode 100644 lint-workflow/src/rules/pinned_job_runner.py create mode 100644 lint-workflow/src/rules/step_approved.py create mode 100644 lint-workflow/src/rules/step_pinned.py create mode 100644 lint-workflow/tests/rules/test_job_environment_prefix.py create mode 100644 lint-workflow/tests/rules/test_pinned_job_runner.py create mode 100644 lint-workflow/tests/rules/test_step_approved.py create mode 100644 lint-workflow/tests/rules/test_step_pinned.py create mode 100644 lint-workflow/tests/test_utils.py diff --git a/lint-workflow/Pipfile.lock b/lint-workflow/Pipfile.lock index 7889f32b..2063b47f 100644 --- a/lint-workflow/Pipfile.lock +++ b/lint-workflow/Pipfile.lock @@ -292,7 +292,7 @@ "sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875", "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412" ], - "markers": "platform_python_implementation == 'CPython' and python_version < '3.13'", + "markers": "python_version < '3.13' and platform_python_implementation == 'CPython'", "version": "==0.2.8" }, "typing-extensions": { diff --git a/lint-workflow/README.md b/lint-workflow/README.md index db9da62b..864cab1c 100644 --- a/lint-workflow/README.md +++ b/lint-workflow/README.md @@ -47,20 +47,20 @@ black . - [x] assert name exists - [x] assert name is capitalized -- [ ] assert runs-on is pinned -- [ ] assert any environment variables start with "_" +- [x] assert runs-on is pinned +- [x] assert any environment variables start with "_" #### shared steps - [x] assert name exists - [x] assert name is capitilized #### uses steps -- [ ] assert valid hash format - correct length -- [ ] assert valid hash format - cast to hexidecimal -- [ ] assert valid action repo path format -- [ ] assert action exists in GitHub -- [ ] warn out of date Action -- [ ] warn using an unapproved Action +- [x] assert valid hash format - correct length +- [x] assert valid hash format - cast to hexidecimal +- [x] warn using an unapproved Action +- [x] warn out of date Action +- [ ] (DEPRECATED) assert action exists in GitHub (deprecated in favor of the approved list of actions) +- [ ] (DEPRECATED) assert valid action repo path format (deprecated in favor fo the approved list of actions) #### run steps - [ ] assert correct format for single line run diff --git a/lint-workflow/actions.json b/lint-workflow/actions.json new file mode 100644 index 00000000..2c326e29 --- /dev/null +++ b/lint-workflow/actions.json @@ -0,0 +1,54 @@ +[ + {"name": "act10ns/slack", "version": "", "sha": ""}, + {"name": "actions/cache", "version": "", "sha": ""}, + {"name": "actions/checkout", "version": "", "sha": ""}, + {"name": "actions/delete-package-versions", "version": "", "sha": ""}, + {"name": "actions/download-artifact", "version": "", "sha": ""}, + {"name": "actions/github-script", "version": "", "sha": ""}, + {"name": "actions/labeler", "version": "", "sha": ""}, + {"name": "actions/setup-dotnet", "version": "", "sha": ""}, + {"name": "actions/setup-java", "version": "", "sha": ""}, + {"name": "actions/setup-node", "version": "", "sha": ""}, + {"name": "actions/setup-python", "version": "", "sha": ""}, + {"name": "actions/stale", "version": "", "sha": ""}, + {"name": "actions/upload-artifact", "version": "", "sha": ""}, + {"name": "android-actions/setup-android", "version": "", "sha": ""}, + {"name": "Asana/create-app-attachment-github-action", "version": "", "sha": ""}, + {"name": "Azure/functions-action", "version": "", "sha": ""}, + {"name": "Azure/get-keyvault-secrets", "version": "", "sha": ""}, + {"name": "Azure/login", "version": "", "sha": ""}, + {"name": "azure/webapps-deploy", "version": "", "sha": ""}, + {"name": "bitwarden/sm-action", "version": "", "sha": ""}, + {"name": "checkmarx/ast-github-action", "version": "", "sha": ""}, + {"name": "chrnorm/deployment-action", "version": "", "sha": ""}, + {"name": "chrnorm/deployment-status", "version": "", "sha": ""}, + {"name": "chromaui/action", "version": "", "sha": ""}, + {"name": "cloudflare/pages-action", "version": "", "sha": ""}, + {"name": "convictional/trigger-workflow-and-wait", "version": "", "sha": ""}, + {"name": "crazy-max/ghaction-import-gpg", "version": "", "sha": ""}, + {"name": "crowdin/github-action", "version": "", "sha": ""}, + {"name": "dawidd6/action-download-artifact", "version": "", "sha": ""}, + {"name": "dawidd6/action-homebrew-bump-formula", "version": "", "sha": ""}, + {"name": "digitalocean/action-doctl", "version": "", "sha": ""}, + {"name": "docker/build-push-action", "version": "", "sha": ""}, + {"name": "docker/setup-buildx-action", "version": "", "sha": ""}, + {"name": "docker/setup-qemu-action", "version": "", "sha": ""}, + {"name": "dorny/test-reporter", "version": "", "sha": ""}, + {"name": "dtolnay/rust-toolchain", "version": "", "sha": ""}, + {"name": "futureware-tech/simulator-action", "version": "", "sha": ""}, + {"name": "hashicorp/setup-packer", "version": "", "sha": ""}, + {"name": "macauley/action-homebrew-bump-cask", "version": "", "sha": ""}, + {"name": "microsoft/setup-msbuild", "version": "", "sha": ""}, + {"name": "ncipollo/release-action", "version": "", "sha": ""}, + {"name": "peter-evans/close-issue", "version": "", "sha": ""}, + {"name": "ruby/setup-ruby", "version": "", "sha": ""}, + {"name": "samuelmeuli/action-snapcraft", "version": "", "sha": ""}, + {"name": "snapcore/action-build", "version": "", "sha": ""}, + {"name": "sonarsource/sonarcloud-github-action", "version": "", "sha": ""}, + {"name": "stackrox/kube-linter-action", "version": "", "sha": ""}, + {"name": "Swatinem/rust-cache", "version": "", "sha": ""}, + {"name": "SwiftDocOrg/github-wiki-publish-action", "version": "", "sha": ""}, + {"name": "SwiftDocOrg/swift-doc", "version": "", "sha": ""}, + {"name": "tj-actions/changed-files", "version": "", "sha": ""}, + {"name": "yogevbd/enforce-label-action", "version": "", "sha": ""} +] diff --git a/lint-workflow/cli.py b/lint-workflow/cli.py index 420a0544..01d0179a 100644 --- a/lint-workflow/cli.py +++ b/lint-workflow/cli.py @@ -6,10 +6,10 @@ import urllib3 as urllib import logging -#from src.rules import workflow_rules, job_rules, step_rules, uses_step_rules, run_step_rules -from settings import enabled_rules +# from src.rules import workflow_rules, job_rules, step_rules, uses_step_rules, run_step_rules +import settings from src.load import WorkflowBuilder, Rules -from src.utils import LintFinding +from src.utils import LintFinding, Settings, SettingsError PROBLEM_LEVELS = { @@ -19,10 +19,22 @@ memoized_action_update_urls = {} +try: + lint_settings = Settings( + enabled_rules=settings.enabled_rules, + approved_actions=settings.approved_actions + ) +except: + raise SettingsError( + ( + "Required settings: enabled_rules, approved_actions\n" + "Please see documentation for more information" + ) + ) -lint_rules = Rules(enabled_rules, verbose=True) +lint_rules = Rules(settings=lint_settings, verbose=True) -#print(lint_rules.workflow) +# print(lint_rules.workflow) class Colors: @@ -240,7 +252,6 @@ def lint(filename): def _old_lint(filename): - findings = [] max_error_level = 0 @@ -332,7 +343,6 @@ def _old_lint(filename): # If the step has a 'uses' key, check value hash. try: - # Check to make sure SHA1 hash is 40 characters. if len(hash) != 40: findings.append( @@ -420,7 +430,6 @@ def _old_lint(filename): def main(input_args=None): - # Pull the arguments from the command line if not input_args: input_args = sys.argv[1:] diff --git a/lint-workflow/lint.py b/lint-workflow/lint.py index 4df014bc..097fc4db 100644 --- a/lint-workflow/lint.py +++ b/lint-workflow/lint.py @@ -30,6 +30,7 @@ class Colors: class LintFinding: """Represents a linting problem.""" + description: str = "" level: str = None @@ -95,7 +96,7 @@ def action_repo_exists(action_id): path, *hash = action_id.split("@") - if "bitwarden/gh-actions" in path: + if "bitwarden" in path: path_list = path.split("/", 2) url = f"https://api.github.com/repos/{path_list[0]}/{path_list[1]}" response = get_github_api_response(url, action_id) @@ -150,7 +151,7 @@ def get_action_update(action_id): if path in memoized_action_update_urls: return memoized_action_update_urls[path] else: - if "bitwarden/gh-actions" in path: + if "bitwarden" in path: path_list = path.split("/", 2) url = f"https://api.github.com/repos/{path_list[0]}/{path_list[1]}/commits?path={path_list[2]}" response = get_github_api_response(url, action_id) @@ -200,11 +201,6 @@ def get_action_update(action_id): def lint(filename): - - supported_actions = {"act10ns/slack", "actions/cache", "actions/checkout", "actions/delete-package-versions", "actions/download-artifact", "actions/github-script", "actions/labeler", "actions/setup-dotnet", "actions/setup-java", "actions/setup-node", "actions/setup-python", "actions/stale", "actions/upload-artifact", "android-actions/setup-android", "Asana/create-app-attachment-github-action", "Azure/functions-action", "Azure/get-keyvault-secrets", "Azure/login", "azure/webapps-deploy", "bitwarden/sm-action", "checkmarx/ast-github-action", "chrnorm/deployment-action", "chrnorm/deployment-status", "chromaui/action", "cloudflare/pages-action", "convictional/trigger-workflow-and-wait", "crazy-max/ghaction-import-gpg", "crowdin/github-action", "dawidd6/action-download-artifact", "dawidd6/action-homebrew-bump-formula", "digitalocean/action-doctl", "docker/build-push-action", "docker/setup-buildx-action", "docker/setup-qemu-action", "dorny/test-reporter", "dtolnay/rust-toolchain", "futureware-tech/simulator-action", "hashicorp/setup-packer", "macauley/action-homebrew-bump-cask", "microsoft/setup-msbuild", "ncipollo/release-action", "peter-evans/close-issue", "ruby/setup-ruby", "samuelmeuli/action-snapcraft", "snapcore/action-build", "sonarsource/sonarcloud-github-action", "stackrox/kube-linter-action", "Swatinem/rust-cache", "SwiftDocOrg/github-wiki-publish-action", "SwiftDocOrg/swift-doc", "tj-actions/changed-files", "yogevbd/enforce-label-action"} - - - findings = [] max_error_level = 0 @@ -294,49 +290,40 @@ def lint(filename): logging.info("Skipping local action in workflow.") break - # If the step has a 'uses' key, check if actions are in supported actions list and also value hash, except bitwarden actions. - if "bitwarden/gh-actions" not in path: - try: - # Check if actions are in supported actions list. - if path not in supported_actions: - findings.append( - LintFinding( - f"Step {str(i)} of job key '{job_key}' uses an unsupported action: {path}.", - "warning", - ) - ) - # Check to make sure SHA1 hash is 40 characters. - if len(hash) != 40: - findings.append( - LintFinding( - f"Step {str(i)} of job key '{job_key}' does not have a valid action hash. (not 40 characters)", - "error", - ) + # If the step has a 'uses' key, check value hash. + try: + # Check to make sure SHA1 hash is 40 characters. + if len(hash) != 40: + findings.append( + LintFinding( + f"Step {str(i)} of job key '{job_key}' does not have a valid action hash. (not 40 characters)", + "error", ) + ) - # Attempts to convert the hash to a integer - # which will succeed if all characters are hexadecimal - try: - int(hash, 16) - except ValueError: - findings.append( - LintFinding( - f"Step {str(i)} of job key '{job_key}' does not have a valid action hash. (not all hexadecimal characters)", - "error", - ) - ) - except: + # Attempts to convert the hash to a integer + # which will succeed if all characters are hexadecimal + try: + int(hash, 16) + except ValueError: findings.append( LintFinding( - f"Step {str(i)} of job key '{job_key}' does not have a valid action hash. (missing '@' character)", + f"Step {str(i)} of job key '{job_key}' does not have a valid action hash. (not all hexadecimal characters)", "error", ) ) + except: + findings.append( + LintFinding( + f"Step {str(i)} of job key '{job_key}' does not have a valid action hash. (missing '@' character)", + "error", + ) + ) # If the step has a 'uses' key, check path for external workflow path_list = path.split("/", 2) - if "bitwarden/gh-actions" in path and len(path_list) < 3: + if "bitwarden" in path and len(path_list) < 3: findings.append( LintFinding( f"Step {str(i)} of job key '{job_key}' does not have a valid action path. (missing name of the repository or workflow)", @@ -392,7 +379,6 @@ def lint(filename): def main(input_args=None): - # Pull the arguments from the command line if not input_args: input_args = sys.argv[1:] diff --git a/lint-workflow/settings.py b/lint-workflow/settings.py index cad8e75f..3d1d2480 100644 --- a/lint-workflow/settings.py +++ b/lint-workflow/settings.py @@ -1,4 +1,63 @@ enabled_rules = [ - 'src.rules.name_exists.RuleNameExists', - 'src.rules.name_capitalized.RuleNameCapitalized' + "src.rules.name_exists.RuleNameExists", + "src.rules.name_capitalized.RuleNameCapitalized", + "src.rules.pinned_job_runner.RuleJobRunnerVersionPinned", + "src.rules.job_environment_prefix.RuleJobEnvironmentPrefix", + "src.rules.step_hex.RuleStepUsesPinned", +] + + +approved_actions = [ + "act10ns/slack", + "actions/cache", + "actions/checkout", + "actions/delete-package-versions", + "actions/download-artifact", + "actions/github-script", + "actions/labeler", + "actions/setup-dotnet", + "actions/setup-java", + "actions/setup-node", + "actions/setup-python", + "actions/stale", + "actions/upload-artifact", + "android-actions/setup-android", + "Asana/create-app-attachment-github-action", + "Azure/functions-action", + "Azure/get-keyvault-secrets", + "Azure/login", + "azure/webapps-deploy", + "bitwarden/sm-action", + "checkmarx/ast-github-action", + "chrnorm/deployment-action", + "chrnorm/deployment-status", + "chromaui/action", + "cloudflare/pages-action", + "convictional/trigger-workflow-and-wait", + "crazy-max/ghaction-import-gpg", + "crowdin/github-action", + "dawidd6/action-download-artifact", + "dawidd6/action-homebrew-bump-formula", + "digitalocean/action-doctl", + "docker/build-push-action", + "docker/setup-buildx-action", + "docker/setup-qemu-action", + "dorny/test-reporter", + "dtolnay/rust-toolchain", + "futureware-tech/simulator-action", + "hashicorp/setup-packer", + "macauley/action-homebrew-bump-cask", + "microsoft/setup-msbuild", + "ncipollo/release-action", + "peter-evans/close-issue", + "ruby/setup-ruby", + "samuelmeuli/action-snapcraft", + "snapcore/action-build", + "sonarsource/sonarcloud-github-action", + "stackrox/kube-linter-action", + "Swatinem/rust-cache", + "SwiftDocOrg/github-wiki-publish-action", + "SwiftDocOrg/swift-doc", + "tj-actions/changed-files", + "yogevbd/enforce-label-action", ] diff --git a/lint-workflow/src/load.py b/lint-workflow/src/load.py index 4614ed57..7270a28a 100644 --- a/lint-workflow/src/load.py +++ b/lint-workflow/src/load.py @@ -8,33 +8,55 @@ from .models.step import Step from .models.workflow import Workflow from .rule import Rule +from .utils import Settings yaml = YAML() +class WorkflowBuilderError(Exception): + pass + + class WorkflowBuilder: @classmethod - def __load_workflow(cls, filename: str) -> CommentedMap: + def __load_workflow_from_file(cls, filename: str) -> CommentedMap: with open(filename) as file: return yaml.load(file) @classmethod - def __build_workflow(cls, loaded_yaml: str) -> Workflow: - return Workflow.from_dict({ - **loaded_yaml, - "jobs": {str(job_key): Job.init(job_key, { - **job, - "steps": [ - Step.init(idx, job_key, step_data) - for idx, step_data in enumerate(job["steps"]) - ] - }) for job_key, job in loaded_yaml["jobs"].items()} - }) + def __build_workflow(cls, loaded_yaml: CommentedMap) -> Workflow: + return Workflow.from_dict( + { + **loaded_yaml, + "jobs": { + str(job_key): Job.init( + job_key, + { + **job, + "steps": [ + Step.init(idx, job_key, step_data) + for idx, step_data in enumerate(job["steps"]) + ], + }, + ) + for job_key, job in loaded_yaml["jobs"].items() + }, + } + ) @classmethod - def build(cls, filename: str) -> Workflow: - return cls.__build_workflow(cls.__load_workflow(filename)) + def build( + cls, filename: str = None, yaml: CommentedMap = None, from_file: bool = True + ) -> Workflow: + if from_file and filename is not None: + return cls.__build_workflow(cls.__load_workflow_from_file(filename)) + elif not from_file and yaml is not None: + return cls.__build_workflow(yaml) + + raise WorkflowBuilderException( + "The workflow must either be built from a file or from a CommentedMap" + ) class Rules: @@ -42,15 +64,15 @@ class Rules: job: List[Rule] = [] step: List[Rule] = [] - def __init__(self, settings: List[str], verbose: bool = False) -> None: - for rule in settings: - module_name = rule.split('.') + def __init__(self, settings: Settings, verbose: bool = False) -> None: + for rule in settings.enabled_rules: + module_name = rule.split(".") module_name = ".".join(module_name[:-1]) - rule_name = rule.split('.')[-1] + rule_name = rule.split(".")[-1] try: - rule_module = getattr(importlib.import_module(module_name), rule_name) - rule_inst = rule_module() + rule_class = getattr(importlib.import_module(module_name), rule_name) + rule_inst = rule_class(settings=settings) if Workflow in rule_inst.compatibility: self.workflow.append(rule_inst) diff --git a/lint-workflow/src/models/job.py b/lint-workflow/src/models/job.py index 35b21336..825a0838 100644 --- a/lint-workflow/src/models/job.py +++ b/lint-workflow/src/models/job.py @@ -6,6 +6,7 @@ from src.models.step import Step + @dataclass_json(undefined=Undefined.EXCLUDE) @dataclass class Job: diff --git a/lint-workflow/src/models/step.py b/lint-workflow/src/models/step.py index 8fb16165..aed2191b 100644 --- a/lint-workflow/src/models/step.py +++ b/lint-workflow/src/models/step.py @@ -13,8 +13,13 @@ class Step: name: Optional[str] = None env: Optional[CommentedMap] = None uses: Optional[str] = None + uses_path: Optional[str] = None + uses_ref: Optional[str] = None uses_comment: Optional[str] = None - uses_with: Optional[CommentedMap] = field(metadata=config(field_name="with"), default=None) + uses_version: Optional[str] = None + uses_with: Optional[CommentedMap] = field( + metadata=config(field_name="with"), default=None + ) run: Optional[str] = None @classmethod @@ -25,6 +30,9 @@ def init(cls, idx: int, job: str, data: CommentedMap) -> Self: new_step.job = job if "uses" in data.ca.items and data.ca.items["uses"][2]: - new_step.uses_comment = data.ca.items["uses"][2].value.replace('\n', '') + new_step.uses_comment = data.ca.items["uses"][2].value.replace("\n", "") + if "@" in new_step.uses: + new_step.uses_path, new_step.uses_ref = new_step.uses.split("@") + new_step.uses_version = new_step.uses_comment.split(" ")[-1] return new_step diff --git a/lint-workflow/src/models/workflow.py b/lint-workflow/src/models/workflow.py index 59da2e5f..859f4619 100644 --- a/lint-workflow/src/models/workflow.py +++ b/lint-workflow/src/models/workflow.py @@ -13,4 +13,4 @@ class Workflow: key: str = "" name: Optional[str] = None on: Optional[CommentedMap] = None - jobs: Optional[CommentedMap] = None + jobs: Optional[CommentedMap] = None diff --git a/lint-workflow/src/rule.py b/lint-workflow/src/rule.py index 993a5b06..8d615dc4 100644 --- a/lint-workflow/src/rule.py +++ b/lint-workflow/src/rule.py @@ -3,16 +3,17 @@ from .models.workflow import Workflow from .models.job import Job from .models.step import Step -from .utils import LintFinding +from .utils import LintFinding, Settings class Rule: message: str = "error" on_fail: str = "error" compatibility: List[Union[Workflow, Job, Step]] = [Workflow, Job, Step] + settings: Settings = None def fn(self, obj: Union[Workflow, Job, Step]) -> bool: - return False + return False, self.message def build_lint_message(self, message: str, obj: Union[Workflow, Job, Step]) -> str: obj_type = type(obj) @@ -25,22 +26,28 @@ def build_lint_message(self, message: str, obj: Union[Workflow, Job, Step]) -> s return f"{obj_type.__name__} => {message}" def execute(self, obj: Union[Workflow, Job, Step]) -> Union[LintFinding, None]: + message = None + if type(obj) not in self.compatibility: return LintFinding( self.build_lint_message( f"{type(obj).__name__} not compatible with {type(self).__name__}", - obj + obj, ), - "error" + "error", ) try: - if self.fn(obj): + passed, message = self.fn(obj) + + if passed: return None - except: + except Exception as err: return LintFinding( - self.build_lint_message(f"failed to apply {type(self).__name__}", obj), - "error" + self.build_lint_message( + f"failed to apply {type(self).__name__}\n{err}", obj + ), + "error", ) - return LintFinding(self.build_lint_message(self.message, obj), self.on_fail) + return LintFinding(self.build_lint_message(message, obj), self.on_fail) diff --git a/lint-workflow/src/rules/job_environment_prefix.py b/lint-workflow/src/rules/job_environment_prefix.py new file mode 100644 index 00000000..18c2268f --- /dev/null +++ b/lint-workflow/src/rules/job_environment_prefix.py @@ -0,0 +1,23 @@ +from ..rule import Rule +from ..models.job import Job + + +class RuleJobEnvironmentPrefix(Rule): + def __init__(self): + self.message = f"Job Environment vars should start with and underscore:\n" + self.on_fail = "error" + self.compability = [Job] + + def fn(self, obj: Job): + correct = True + message = "" + + for key, value in obj.env.items(): + if key[0] != "_": + message += f" {key}" + correct = False + + if correct: + return True, "" + + return False, f"{self.message}{message}" diff --git a/lint-workflow/src/rules/name_capitalized.py b/lint-workflow/src/rules/name_capitalized.py index 4df12360..609aff5f 100644 --- a/lint-workflow/src/rules/name_capitalized.py +++ b/lint-workflow/src/rules/name_capitalized.py @@ -13,5 +13,5 @@ def __init__(self): def fn(self, obj: Union[Workflow, Job, Step]): if obj.name: - return obj.name[0].isupper() - return True # Force passing if obj.name doesn't exist + return obj.name[0].isupper(), self.message + return True, "" # Force passing if obj.name doesn't exist diff --git a/lint-workflow/src/rules/name_exists.py b/lint-workflow/src/rules/name_exists.py index 7d81fd51..d7b7398c 100644 --- a/lint-workflow/src/rules/name_exists.py +++ b/lint-workflow/src/rules/name_exists.py @@ -12,4 +12,6 @@ def __init__(self): self.on_fail = "error" def fn(self, obj: Union[Workflow, Job, Step]): - return obj.name is not None + if obj.name is not None: + return True, "" + return False, self.message diff --git a/lint-workflow/src/rules/pinned_job_runner.py b/lint-workflow/src/rules/pinned_job_runner.py new file mode 100644 index 00000000..95919c02 --- /dev/null +++ b/lint-workflow/src/rules/pinned_job_runner.py @@ -0,0 +1,14 @@ +from ..rule import Rule +from ..models.job import Job + + +class RuleJobRunnerVersionPinned(Rule): + def __init__(self): + self.message = "Workflow runner must be pinned" + self.on_fail = "error" + self.compatibility = [Job] + + def fn(self, obj: Job): + if "latest" not in obj.runs_on: + return True, "" + return False, self.message diff --git a/lint-workflow/src/rules/step_approved.py b/lint-workflow/src/rules/step_approved.py new file mode 100644 index 00000000..9df84048 --- /dev/null +++ b/lint-workflow/src/rules/step_approved.py @@ -0,0 +1,56 @@ +from typing import Tuple + +from ..rule import Rule +from ..models.step import Step +from ..utils import Settings + + +class RuleStepUsesApproved(Rule): + def __init__(self, settings: Settings = None) -> None: + self.message = f"error" + self.on_fail = "warn" + self.compatibility = [Step] + self.settings = settings + + def force_pass(self, obj: Step) -> bool: + ## Force pass for any shell steps + if not obj.uses: + return True, "" + + ## Force pass for any local actions + if "@" not in obj.uses: + return True + + ## Force pass for any bitwarden/gh-actions + if obj.uses.startswith("bitwarden/gh-actions"): + return True + + return False + + def fn(self, obj: Step) -> Tuple[bool, str]: + if self.force_pass(obj): + return True, "" + + path, hash = obj.uses.split("@") + + # Actions in bitwarden/gh-actions are auto-approved + if not path in self.settings.approved_actions: + return ( + False, + ( + f"New Action detected: {path}\n" + "For security purposes, actions must be reviewed and on the pre-approved list" + ), + ) + + action_data = self.settings.approved_actions_data[path] + + if obj.uses_version != action_data["version"] or obj.uses_ref != action_data["sha"]: + return False, ( + "Action is out of date. Please update to:\n" + f" commit: {action_data['version']}" + f" version: {action_data['sha']}" + ) + + + return True, "" diff --git a/lint-workflow/src/rules/step_pinned.py b/lint-workflow/src/rules/step_pinned.py new file mode 100644 index 00000000..cc80b28d --- /dev/null +++ b/lint-workflow/src/rules/step_pinned.py @@ -0,0 +1,42 @@ +from typing import Tuple + +from ..rule import Rule +from ..models.step import Step + + +class RuleStepUsesPinned(Rule): + def __init__(self) -> None: + self.message = f"error" + self.on_fail = "error" + self.compatibility = [Step] + + def force_pass(self, obj: Step) -> bool: + if not obj.uses: + return True, "" + + ## Force pass for any local actions + if "@" not in obj.uses: + return True + + return False + + def fn(self, obj: Step) -> Tuple[bool, str]: + if self.force_pass(obj): + return True, "" + + path, ref = obj.uses.split("@") + + if path.startswith("bitwarden/gh-actions"): + if ref == "main": + return True, "" + return False, "Please pin to main" + + try: + int(ref, 16) + except: + return False, "Please pin the action to a commit sha" + + if len(ref) != 40: + return False, f"Please use the full commit sha to pin the action" + + return True, "" diff --git a/lint-workflow/src/utils.py b/lint-workflow/src/utils.py index 800169eb..8f795888 100644 --- a/lint-workflow/src/utils.py +++ b/lint-workflow/src/utils.py @@ -4,5 +4,28 @@ @dataclass class LintFinding: """Represents a linting problem.""" + description: str = "" level: str = None + + +@dataclass +class Settings: + enabled_rules: list[str] + approved_actions_keys: set[str] + approved_actions_data: dict[str, str] + + def __init__( + self, + enabled_rules: list[str] = None, + approved_actions: list[dict[str, str]]= None + ): + self.enabled_rules = enabled_rules + self.approved_actions = set([action['name'] for action in approved_actions]) + self.approved_actions_data = { + action['name']: action for action in approved_actions + } + + +class SettingsError(Exception): + pass diff --git a/lint-workflow/tests/conftest.py b/lint-workflow/tests/conftest.py index 2315e5ef..eb3c64df 100644 --- a/lint-workflow/tests/conftest.py +++ b/lint-workflow/tests/conftest.py @@ -1 +1 @@ -FIXTURE_DIR="./tests/fixtures" +FIXTURE_DIR = "./tests/fixtures" diff --git a/lint-workflow/tests/context.py b/lint-workflow/tests/context.py index 8fa51c8a..7d326ca8 100644 --- a/lint-workflow/tests/context.py +++ b/lint-workflow/tests/context.py @@ -1,5 +1,6 @@ import sys import os -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) import src diff --git a/lint-workflow/tests/fixtures/test_a.yaml b/lint-workflow/tests/fixtures/test_a.yaml index f0494a11..bd0cfb24 100644 --- a/lint-workflow/tests/fixtures/test_a.yaml +++ b/lint-workflow/tests/fixtures/test_a.yaml @@ -7,7 +7,7 @@ on: jobs: call-workflow: - uses: bitwarden/server/.github/workflows/workflow-linter.yml@main + uses: bitwarden/server/.github/workflows/workflow-linter.yml@master test-normal-action: name: Download Latest diff --git a/lint-workflow/tests/rules/test_job_environment_prefix.py b/lint-workflow/tests/rules/test_job_environment_prefix.py new file mode 100644 index 00000000..c77d3ef4 --- /dev/null +++ b/lint-workflow/tests/rules/test_job_environment_prefix.py @@ -0,0 +1,64 @@ +import pytest + +from ruamel.yaml import YAML + +from ..conftest import FIXTURE_DIR +from ..context import src + +from src.load import WorkflowBuilder +from src.rules.job_environment_prefix import RuleJobEnvironmentPrefix + +yaml = YAML() + + +@pytest.fixture +def correct_workflow(): + workflow = """\ +--- +on: + workflow_dispatch: + +jobs: + job-key: + runs-on: ubuntu-22.04 + env: + _TEST_ENV: "test" + steps: + - run: echo test +""" + return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) + + +@pytest.fixture +def incorrect_workflow(): + workflow = """\ +--- +on: + workflow_dispatch: + +jobs: + job-key: + runs-on: ubuntu-22.04 + env: + TEST_ENV: "test" + steps: + - run: echo test +""" + return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) + + +@pytest.fixture +def rule(): + return RuleJobEnvironmentPrefix() + + +def test_rule_on_correct_workflow(rule, correct_workflow): + result, message = rule.fn(correct_workflow.jobs["job-key"]) + assert result == True + assert message == "" + + +def test_rule_on_incorrect_workflow(rule, incorrect_workflow): + result, message = rule.fn(incorrect_workflow.jobs["job-key"]) + assert result == False + assert "TEST_ENV" in message diff --git a/lint-workflow/tests/rules/test_name_capitalized.py b/lint-workflow/tests/rules/test_name_capitalized.py index c5ca64b7..bee36fae 100644 --- a/lint-workflow/tests/rules/test_name_capitalized.py +++ b/lint-workflow/tests/rules/test_name_capitalized.py @@ -1,11 +1,15 @@ import pytest +from ruamel.yaml import YAML + from ..conftest import FIXTURE_DIR from ..context import src from src.load import WorkflowBuilder from src.rules.name_capitalized import RuleNameCapitalized +yaml = YAML() + @pytest.fixture def correct_workflow(): @@ -13,8 +17,60 @@ def correct_workflow(): @pytest.fixture -def incorrect_workflow(): - return WorkflowBuilder.build(f"{FIXTURE_DIR}/test-min-incorrect.yaml") +def incorrect_workflow_name(): + workflow = """\ +--- +name: test +on: + workflow_dispatch: + +jobs: + job-key: + name: Test + runs-on: ubuntu-latest + steps: + - name: Test + run: echo test +""" + return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) + + +@pytest.fixture +def incorrect_job_name(): + workflow = """\ +--- +name: Test +on: + workflow_dispatch: + +jobs: + job-key: + name: test + runs-on: ubuntu-latest + steps: + - name: Test + run: echo test +""" + return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) + + +@pytest.fixture +def incorrect_step_name(): + workflow = """\ +--- +name: Test +on: + workflow_dispatch: + +jobs: + job-key: + name: Test + runs-on: ubuntu-latest + steps: + - name: test + run: echo test +""" + return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) @pytest.fixture @@ -23,13 +79,26 @@ def rule(): def test_rule_on_correct_workflow(rule, correct_workflow): - assert rule.fn(correct_workflow) == True - assert rule.fn(correct_workflow.jobs['job-key']) == True - assert rule.fn(correct_workflow.jobs['job-key'].steps[0]) == True + result, message = rule.fn(correct_workflow) + assert result == True + + result, message = rule.fn(correct_workflow.jobs["job-key"]) + assert result == True + + result, message = rule.fn(correct_workflow.jobs["job-key"].steps[0]) + assert result == True + + +def test_rule_on_incorrect_workflow_name(rule, incorrect_workflow_name): + result, message = rule.fn(incorrect_workflow_name) + assert result == False + + +def test_rule_on_incorrect_workflow_name(rule, incorrect_job_name): + result, message = rule.fn(incorrect_job_name.jobs["job-key"]) + assert result == False -def test_rule_on_incorrect_workflow(rule, incorrect_workflow): - print(f"Workflow name: {incorrect_workflow.name}") - assert rule.fn(incorrect_workflow) == False - assert rule.fn(incorrect_workflow.jobs['job-key']) == False - assert rule.fn(incorrect_workflow.jobs['job-key'].steps[0]) == False +def test_rule_on_incorrect_workflow_name(rule, incorrect_step_name): + result, message = rule.fn(incorrect_step_name.jobs["job-key"].steps[0]) + assert result == False diff --git a/lint-workflow/tests/rules/test_name_exists.py b/lint-workflow/tests/rules/test_name_exists.py index c8631bb8..3853326a 100644 --- a/lint-workflow/tests/rules/test_name_exists.py +++ b/lint-workflow/tests/rules/test_name_exists.py @@ -23,13 +23,23 @@ def rule(): def test_rule_on_correct_workflow(rule, correct_workflow): - assert rule.fn(correct_workflow) == True - assert rule.fn(correct_workflow.jobs['job-key']) == True - assert rule.fn(correct_workflow.jobs['job-key'].steps[0]) == True + result, message = rule.fn(correct_workflow) + assert result == True + + result, message = rule.fn(correct_workflow.jobs["job-key"]) + assert result == True + + result, message = rule.fn(correct_workflow.jobs["job-key"].steps[0]) + assert result == True def test_rule_on_incorrect_workflow(rule, incorrect_workflow): print(f"Workflow name: {incorrect_workflow.name}") - assert rule.fn(incorrect_workflow) == False - assert rule.fn(incorrect_workflow.jobs['job-key']) == False - assert rule.fn(incorrect_workflow.jobs['job-key'].steps[0]) == False + result, message = rule.fn(incorrect_workflow) + assert result == False + + result, message = rule.fn(incorrect_workflow.jobs["job-key"]) + assert result == False + + result, message = rule.fn(incorrect_workflow.jobs["job-key"].steps[0]) + assert result == False diff --git a/lint-workflow/tests/rules/test_pinned_job_runner.py b/lint-workflow/tests/rules/test_pinned_job_runner.py new file mode 100644 index 00000000..18cd1fc1 --- /dev/null +++ b/lint-workflow/tests/rules/test_pinned_job_runner.py @@ -0,0 +1,58 @@ +import pytest + +from ruamel.yaml import YAML + +from ..conftest import FIXTURE_DIR +from ..context import src + +from src.load import WorkflowBuilder +from src.rules.pinned_job_runner import RuleJobRunnerVersionPinned + +yaml = YAML() + + +@pytest.fixture +def correct_runner(): + workflow = """\ +--- +on: + workflow_dispatch: + +jobs: + job-key: + runs-on: ubuntu-22.04 + steps: + - run: echo test +""" + return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) + + +@pytest.fixture +def incorrect_runner(): + workflow = """\ +--- +on: + workflow_dispatch: + +jobs: + job-key: + runs-on: ubuntu-latest + steps: + - run: echo test +""" + return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) + + +@pytest.fixture +def rule(): + return RuleJobRunnerVersionPinned() + + +def test_rule_on_correct_runner(rule, correct_runner): + result, message = rule.fn(correct_runner.jobs["job-key"]) + assert result == True + + +def test_rule_on_incorrect_runner(rule, incorrect_runner): + result, message = rule.fn(incorrect_runner.jobs["job-key"]) + assert result == False diff --git a/lint-workflow/tests/rules/test_step_approved.py b/lint-workflow/tests/rules/test_step_approved.py new file mode 100644 index 00000000..f0ea7e0a --- /dev/null +++ b/lint-workflow/tests/rules/test_step_approved.py @@ -0,0 +1,92 @@ +import pytest + +from ruamel.yaml import YAML + +from ..conftest import FIXTURE_DIR +from ..context import src + +from src.load import WorkflowBuilder +from src.rules.step_approved import RuleStepUsesApproved +from src.utils import Settings + + +yaml = YAML() + + +@pytest.fixture +def settings(): + return Settings(approved_actions=[ + { + "name": "actions/checkout", + "version": "v4.1.1", + "sha": "b4ffde65f46336ab88eb53be808477a3936bae11" + }, + { + "name": "actions/download-artifact", + "version": "v4.1.0", + "sha": "f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110" + } + ]) + + +@pytest.fixture +def correct_workflow(): + workflow = """\ +--- +on: + workflow_dispatch: + +jobs: + job-key: + runs-on: ubuntu-22.04 + steps: + - name: Checkout Branch + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Test Bitwarden Action + uses: bitwarden/gh-actions/get-keyvault-secrets@main +""" + return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) + + +@pytest.fixture +def incorrect_workflow(): + workflow = """\ +--- +on: + workflow_dispatch: + +jobs: + job-key: + runs-on: ubuntu-22.04 + steps: + - name: Checkout Branch + uses: joseph-flinn/action-DNE@main + + - name: Out of date action + uses: actions/download-artifact@7a1cd3216ca9260cd8022db641d960b1db4d1be4 # v4.0.0 +""" + return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) + + +@pytest.fixture +def rule(settings): + return RuleStepUsesApproved(settings=settings) + + +def test_rule_on_correct_workflow(rule, correct_workflow): + result, message = rule.fn(correct_workflow.jobs["job-key"].steps[0]) + assert result == True + + result, message = rule.fn(correct_workflow.jobs["job-key"].steps[1]) + assert result == True + + +def test_rule_on_incorrect_workflow(rule, incorrect_workflow): + result, message = rule.fn(incorrect_workflow.jobs["job-key"].steps[0]) + assert result == False + assert "New Action detected" in message + + result, message = rule.fn(incorrect_workflow.jobs["job-key"].steps[1]) + assert result == False + assert "Action is out of date" in message diff --git a/lint-workflow/tests/rules/test_step_pinned.py b/lint-workflow/tests/rules/test_step_pinned.py new file mode 100644 index 00000000..1a95f025 --- /dev/null +++ b/lint-workflow/tests/rules/test_step_pinned.py @@ -0,0 +1,86 @@ +import pytest + +from ruamel.yaml import YAML + +from ..conftest import FIXTURE_DIR +from ..context import src + +from src.load import WorkflowBuilder +from src.rules.step_pinned import RuleStepUsesPinned + +yaml = YAML() + + +@pytest.fixture +def correct_workflow(): + workflow = """\ +--- +on: + workflow_dispatch: + +jobs: + job-key: + runs-on: ubuntu-22.04 + steps: + - name: Test 3rd Party Action + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Test Internal Action + uses: bitwarden/gh-actions/get-keyvault-secrets@main +""" + return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) + + +@pytest.fixture +def incorrect_workflow(): + workflow = """\ +--- +on: + workflow_dispatch: + +jobs: + job-key: + runs-on: ubuntu-22.04 + steps: + - name: Test External Branch + uses: actions/checkout@main + + - name: Test Incorrect Hex + uses: actions/checkout@b4ffde + + - name: Test Internal Commit + uses: bitwarden/gh-actions/get-keyvault-secrets@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 +""" + return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) + + + +@pytest.fixture +def rule(): + return RuleStepUsesPinned() + + +def test_rule_on_correct_workflow(rule, correct_workflow): + result, message = rule.fn(correct_workflow.jobs["job-key"].steps[0]) + assert result == True + + result, message = rule.fn(correct_workflow.jobs["job-key"].steps[1]) + assert result == True + + +def test_rule_on_incorrect_workflow_external_branch(rule, incorrect_workflow): + result, message = rule.fn(incorrect_workflow.jobs["job-key"].steps[0]) + assert result == False + assert "Please pin the action" in message + + +def test_rule_on_incorrect_workflow_hex(rule, incorrect_workflow): + result, message = rule.fn(incorrect_workflow.jobs["job-key"].steps[1]) + assert result == False + assert "Please use the full commit sha" in message + + +def test_rule_on_incorrect_workflow_internal_commit(rule, incorrect_workflow): + result, message = rule.fn(incorrect_workflow.jobs["job-key"].steps[2]) + assert result == False + assert "Please pin to main" in message diff --git a/lint-workflow/tests/test_job.py b/lint-workflow/tests/test_job.py index 4789c746..aafb1527 100644 --- a/lint-workflow/tests/test_job.py +++ b/lint-workflow/tests/test_job.py @@ -9,13 +9,13 @@ def default_job_data(): return { "name": "Test", "runs-on": "ubuntu-latest", - "steps": [src.models.Step(run="echo stub")] + "steps": [src.models.Step(run="echo stub")], } @pytest.fixture def default_job(default_job_data): - return src.models.Job.init('default-job', default_job_data) + return src.models.Job.init("default-job", default_job_data) def test_job_default(default_job): @@ -27,10 +27,7 @@ def test_job_default(default_job): def test_job_extra_kwargs(default_job_data): - job = src.models.Job.init('test-job', { - "extra": "test", - **default_job_data - }) + job = src.models.Job.init("test-job", {"extra": "test", **default_job_data}) with pytest.raises(Exception) as e_info: assert job.extra == "test" diff --git a/lint-workflow/tests/test_load.py b/lint-workflow/tests/test_load.py index f64b4016..7ed6317e 100644 --- a/lint-workflow/tests/test_load.py +++ b/lint-workflow/tests/test_load.py @@ -1,17 +1,47 @@ import json import pytest +from ruamel.yaml import YAML from ruamel.yaml.comments import CommentedMap from .conftest import FIXTURE_DIR from .context import src +yaml = YAML() + + @pytest.fixture def workflow_filename(): return f"{FIXTURE_DIR}/test.yml" -def test_load_workflow(workflow_filename): +@pytest.fixture +def workflow_yaml(): + return yaml.load( + """\ +--- +name: test +on: + workflow_dispatch: + +jobs: + job-key: + name: Test + runs-on: ubuntu-latest + steps: + - name: Test + run: echo test +""" + ) + + +def test_load_workflow_from_file(workflow_filename: str) -> None: workflow = src.load.WorkflowBuilder.build(workflow_filename) assert type(workflow) == src.models.Workflow + + +def test_load_workflow_from_yaml(workflow_yaml: CommentedMap) -> None: + workflow = src.load.WorkflowBuilder.build(yaml=workflow_yaml, from_file=False) + + assert type(workflow) == src.models.Workflow diff --git a/lint-workflow/tests/test_rule.py b/lint-workflow/tests/test_rule.py index 59accc1d..5fb5e782 100644 --- a/lint-workflow/tests/test_rule.py +++ b/lint-workflow/tests/test_rule.py @@ -11,6 +11,45 @@ from src.models import Workflow, Job, Step +yaml = YAML() + + +@pytest.fixture +def correct_workflow(): + workflow = """\ +--- +name: Test Workflow + +on: + workflow_dispatch: + +jobs: + job-key: + name: Test + runs-on: ubuntu-latest + steps: + - name: Test + uses: actions/checkout@main +""" + return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) + + +@pytest.fixture +def incorrect_workflow(): + workflow = """\ +--- +on: + workflow_dispatch: + +jobs: + job-key: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@main +""" + return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) + + class RuleStep(Rule): def __init__(self): self.message = "test" @@ -25,7 +64,7 @@ def __init__(self): def fn(self, obj: Union[Workflow, Job, Step]) -> bool: print(f"{type(self).__name__}\n{obj}") - return obj.name is not None + return obj.name is not None, self.message @pytest.fixture @@ -38,45 +77,45 @@ def exists_rule(): return RuleNameExists() -@pytest.fixture -def correct_workflow(): - return WorkflowBuilder.build(f"{FIXTURE_DIR}/test-min.yaml") - - -@pytest.fixture -def incorrect_workflow(): - return WorkflowBuilder.build(f"{FIXTURE_DIR}/test-min-incorrect.yaml") - - def test_build_lint_message(step_rule, correct_workflow): - assert step_rule.build_lint_message( - "test", correct_workflow - ) == "Workflow => test" + assert step_rule.build_lint_message("test", correct_workflow) == "Workflow => test" - assert step_rule.build_lint_message( - "test", correct_workflow.jobs['job-key'] - ) == "Job [job-key] => test" + assert ( + step_rule.build_lint_message("test", correct_workflow.jobs["job-key"]) + == "Job [job-key] => test" + ) - assert step_rule.build_lint_message( - "test", correct_workflow.jobs['job-key'].steps[0] - ) == "Step [job-key.0] => test" + assert ( + step_rule.build_lint_message("test", correct_workflow.jobs["job-key"].steps[0]) + == "Step [job-key.0] => test" + ) def test_rule_compatibility(step_rule, correct_workflow): assert "not compatible" in step_rule.execute(correct_workflow).description - assert "not compatible" in step_rule.execute(correct_workflow.jobs['job-key']).description - assert "not compatible" not in step_rule.execute( - correct_workflow.jobs['job-key'].steps[0] - ).description + assert ( + "not compatible" + in step_rule.execute(correct_workflow.jobs["job-key"]).description + ) + assert ( + "not compatible" + not in step_rule.execute(correct_workflow.jobs["job-key"].steps[0]).description + ) def test_correct_rule_execution(exists_rule, correct_workflow): assert exists_rule.execute(correct_workflow) == None - assert exists_rule.execute(correct_workflow.jobs['job-key']) == None - assert exists_rule.execute(correct_workflow.jobs['job-key'].steps[0]) == None + assert exists_rule.execute(correct_workflow.jobs["job-key"]) == None + assert exists_rule.execute(correct_workflow.jobs["job-key"].steps[0]) == None def test_incorrect_rule_execution(exists_rule, incorrect_workflow): assert "name must exist" in exists_rule.execute(incorrect_workflow).description - assert "name must exist" in exists_rule.execute(incorrect_workflow.jobs['job-key']).description - assert "name must exist" in exists_rule.execute(incorrect_workflow.jobs['job-key'].steps[0]).description + assert ( + "name must exist" + in exists_rule.execute(incorrect_workflow.jobs["job-key"]).description + ) + assert ( + "name must exist" + in exists_rule.execute(incorrect_workflow.jobs["job-key"].steps[0]).description + ) diff --git a/lint-workflow/tests/test_step.py b/lint-workflow/tests/test_step.py index 9a69cad2..4e297a07 100644 --- a/lint-workflow/tests/test_step.py +++ b/lint-workflow/tests/test_step.py @@ -41,7 +41,7 @@ def test_step_default(default_step): assert default_step.env == None assert default_step.uses == None assert default_step.uses_with == None - assert default_step.run == "echo \"test\"" + assert default_step.run == 'echo "test"' def test_step_no_keyword_field(default_step): @@ -59,7 +59,7 @@ def test_step_keyword_field(uses_step): "workflow": "upload-test-artifacts.yml", "artifacts": "artifact", "path": "artifact", - "branch": "main" + "branch": "main", } step_json = uses_step.to_json() @@ -69,7 +69,6 @@ def test_step_keyword_field(uses_step): assert json.loads(uses_step.to_json())["with"] == expected_response - def test_step_comment(uses_step): assert uses_step.key == 0 assert uses_step.job == "default" diff --git a/lint-workflow/tests/test_utils.py b/lint-workflow/tests/test_utils.py new file mode 100644 index 00000000..87c6d5fa --- /dev/null +++ b/lint-workflow/tests/test_utils.py @@ -0,0 +1,8 @@ +import json +import pytest + +from ruamel.yaml import YAML +from ruamel.yaml.comments import CommentedMap + +from .conftest import FIXTURE_DIR +from .context import src diff --git a/lint-workflow/tests/test_workflow.py b/lint-workflow/tests/test_workflow.py index 0d97fc09..0e198e41 100644 --- a/lint-workflow/tests/test_workflow.py +++ b/lint-workflow/tests/test_workflow.py @@ -11,12 +11,14 @@ def workflow_default_data(): "name": "Test Workflow", "on": {}, "jobs": { - "job-key": src.models.Job.from_dict({ - "name": "Test", - "runs-on": "ubuntu-latest", - "steps": [src.models.Step.from_dict({ "run": "echo stub"})] - }) - } + "job-key": src.models.Job.from_dict( + { + "name": "Test", + "runs-on": "ubuntu-latest", + "steps": [src.models.Step.from_dict({"run": "echo stub"})], + } + ) + }, } @@ -29,10 +31,7 @@ def test_workflow_default(workflow_default_data): def test_workflow_extra_kwargs(workflow_default_data): - workflow = src.models.Workflow.from_dict({ - "extra": "test", - **workflow_default_data - }) + workflow = src.models.Workflow.from_dict({"extra": "test", **workflow_default_data}) with pytest.raises(Exception) as e_info: assert workflow.extra == "test" From e7943e22156fbf5d68e8568c0575782e866153a5 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 5 Jan 2024 11:20:19 -0800 Subject: [PATCH 10/57] Add sub commands to manage the list of approved actions --- lint-workflow/.gitignore | 1 + lint-workflow/Pipfile | 2 + lint-workflow/Pipfile.lock | 74 ++- lint-workflow/Session.vim | 294 +++++++++-- lint-workflow/Taskfile.yml | 29 ++ lint-workflow/actions.json | 316 ++++++++++-- lint-workflow/cli.py | 478 ++---------------- lint-workflow/settings.py | 62 +-- lint-workflow/src/actions.py | 132 +++++ lint-workflow/src/lint.py | 104 ++++ lint-workflow/src/load.py | 4 +- .../src/rules/job_environment_prefix.py | 18 +- lint-workflow/src/rules/name_capitalized.py | 8 +- lint-workflow/src/rules/name_exists.py | 8 +- lint-workflow/src/rules/pinned_job_runner.py | 8 +- lint-workflow/src/rules/step_approved.py | 9 +- lint-workflow/src/rules/step_pinned.py | 4 +- lint-workflow/src/utils.py | 50 +- .../rules/test_job_environment_prefix.py | 20 +- .../tests/rules/test_step_approved.py | 24 +- lint-workflow/tests/rules/test_step_pinned.py | 1 - 21 files changed, 1013 insertions(+), 633 deletions(-) create mode 100644 lint-workflow/.gitignore create mode 100644 lint-workflow/Taskfile.yml create mode 100644 lint-workflow/src/actions.py create mode 100644 lint-workflow/src/lint.py diff --git a/lint-workflow/.gitignore b/lint-workflow/.gitignore new file mode 100644 index 00000000..6350e986 --- /dev/null +++ b/lint-workflow/.gitignore @@ -0,0 +1 @@ +.coverage diff --git a/lint-workflow/Pipfile b/lint-workflow/Pipfile index 06bf5640..6c0c5757 100644 --- a/lint-workflow/Pipfile +++ b/lint-workflow/Pipfile @@ -13,6 +13,8 @@ dataclasses-json = "*" [dev-packages] black = "*" pytest = "*" +coverage = "*" +pytest-cov = "*" [requires] python_version = "3.11" diff --git a/lint-workflow/Pipfile.lock b/lint-workflow/Pipfile.lock index 2063b47f..8635bd2a 100644 --- a/lint-workflow/Pipfile.lock +++ b/lint-workflow/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "889524a1d31670cc531521bb7ce622380b376403b3c556248a5df8be064fbb64" + "sha256": "9c217f65befc18770734bee9ec6ee8420cdbad03a5eba07d13a77d3f0d082712" }, "pipfile-spec": 6, "requires": { @@ -356,6 +356,64 @@ "markers": "python_version >= '3.7'", "version": "==8.1.7" }, + "coverage": { + "hashes": [ + "sha256:04387a4a6ecb330c1878907ce0dc04078ea72a869263e53c72a1ba5bbdf380ca", + "sha256:0676cd0ba581e514b7f726495ea75aba3eb20899d824636c6f59b0ed2f88c471", + "sha256:0e8d06778e8fbffccfe96331a3946237f87b1e1d359d7fbe8b06b96c95a5407a", + "sha256:0eb3c2f32dabe3a4aaf6441dde94f35687224dfd7eb2a7f47f3fd9428e421058", + "sha256:109f5985182b6b81fe33323ab4707011875198c41964f014579cf82cebf2bb85", + "sha256:13eaf476ec3e883fe3e5fe3707caeb88268a06284484a3daf8250259ef1ba143", + "sha256:164fdcc3246c69a6526a59b744b62e303039a81e42cfbbdc171c91a8cc2f9446", + "sha256:26776ff6c711d9d835557ee453082025d871e30b3fd6c27fcef14733f67f0590", + "sha256:26f66da8695719ccf90e794ed567a1549bb2644a706b41e9f6eae6816b398c4a", + "sha256:29f3abe810930311c0b5d1a7140f6395369c3db1be68345638c33eec07535105", + "sha256:316543f71025a6565677d84bc4df2114e9b6a615aa39fb165d697dba06a54af9", + "sha256:36b0ea8ab20d6a7564e89cb6135920bc9188fb5f1f7152e94e8300b7b189441a", + "sha256:3cc9d4bc55de8003663ec94c2f215d12d42ceea128da8f0f4036235a119c88ac", + "sha256:485e9f897cf4856a65a57c7f6ea3dc0d4e6c076c87311d4bc003f82cfe199d25", + "sha256:5040148f4ec43644702e7b16ca864c5314ccb8ee0751ef617d49aa0e2d6bf4f2", + "sha256:51456e6fa099a8d9d91497202d9563a320513fcf59f33991b0661a4a6f2ad450", + "sha256:53d7d9158ee03956e0eadac38dfa1ec8068431ef8058fe6447043db1fb40d932", + "sha256:5a10a4920def78bbfff4eff8a05c51be03e42f1c3735be42d851f199144897ba", + "sha256:5b14b4f8760006bfdb6e08667af7bc2d8d9bfdb648351915315ea17645347137", + "sha256:5b2ccb7548a0b65974860a78c9ffe1173cfb5877460e5a229238d985565574ae", + "sha256:697d1317e5290a313ef0d369650cfee1a114abb6021fa239ca12b4849ebbd614", + "sha256:6ae8c9d301207e6856865867d762a4b6fd379c714fcc0607a84b92ee63feff70", + "sha256:707c0f58cb1712b8809ece32b68996ee1e609f71bd14615bd8f87a1293cb610e", + "sha256:74775198b702868ec2d058cb92720a3c5a9177296f75bd97317c787daf711505", + "sha256:756ded44f47f330666843b5781be126ab57bb57c22adbb07d83f6b519783b870", + "sha256:76f03940f9973bfaee8cfba70ac991825611b9aac047e5c80d499a44079ec0bc", + "sha256:79287fd95585ed36e83182794a57a46aeae0b64ca53929d1176db56aacc83451", + "sha256:799c8f873794a08cdf216aa5d0531c6a3747793b70c53f70e98259720a6fe2d7", + "sha256:7d360587e64d006402b7116623cebf9d48893329ef035278969fa3bbf75b697e", + "sha256:80b5ee39b7f0131ebec7968baa9b2309eddb35b8403d1869e08f024efd883566", + "sha256:815ac2d0f3398a14286dc2cea223a6f338109f9ecf39a71160cd1628786bc6f5", + "sha256:83c2dda2666fe32332f8e87481eed056c8b4d163fe18ecc690b02802d36a4d26", + "sha256:846f52f46e212affb5bcf131c952fb4075b55aae6b61adc9856222df89cbe3e2", + "sha256:936d38794044b26c99d3dd004d8af0035ac535b92090f7f2bb5aa9c8e2f5cd42", + "sha256:9864463c1c2f9cb3b5db2cf1ff475eed2f0b4285c2aaf4d357b69959941aa555", + "sha256:995ea5c48c4ebfd898eacb098164b3cc826ba273b3049e4a889658548e321b43", + "sha256:a1526d265743fb49363974b7aa8d5899ff64ee07df47dd8d3e37dcc0818f09ed", + "sha256:a56de34db7b7ff77056a37aedded01b2b98b508227d2d0979d373a9b5d353daa", + "sha256:a7c97726520f784239f6c62506bc70e48d01ae71e9da128259d61ca5e9788516", + "sha256:b8e99f06160602bc64da35158bb76c73522a4010f0649be44a4e167ff8555952", + "sha256:bb1de682da0b824411e00a0d4da5a784ec6496b6850fdf8c865c1d68c0e318dd", + "sha256:bf477c355274a72435ceb140dc42de0dc1e1e0bf6e97195be30487d8eaaf1a09", + "sha256:bf635a52fc1ea401baf88843ae8708591aa4adff875e5c23220de43b1ccf575c", + "sha256:bfd5db349d15c08311702611f3dccbef4b4e2ec148fcc636cf8739519b4a5c0f", + "sha256:c530833afc4707fe48524a44844493f36d8727f04dcce91fb978c414a8556cc6", + "sha256:cc6d65b21c219ec2072c1293c505cf36e4e913a3f936d80028993dd73c7906b1", + "sha256:cd3c1e4cb2ff0083758f09be0f77402e1bdf704adb7f89108007300a6da587d0", + "sha256:cfd2a8b6b0d8e66e944d47cdec2f47c48fef2ba2f2dff5a9a75757f64172857e", + "sha256:d0ca5c71a5a1765a0f8f88022c52b6b8be740e512980362f7fdbb03725a0d6b9", + "sha256:e7defbb9737274023e2d7af02cac77043c86ce88a907c58f42b580a97d5bcca9", + "sha256:e9d1bf53c4c8de58d22e0e956a79a5b37f754ed1ffdbf1a260d9dcfa2d8a325e", + "sha256:ea81d8f9691bb53f4fb4db603203029643caffc82bf998ab5b59ca05560f4c06" + ], + "index": "pypi", + "version": "==7.4.0" + }, "iniconfig": { "hashes": [ "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", @@ -406,11 +464,19 @@ }, "pytest": { "hashes": [ - "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac", - "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5" + "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", + "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8" ], "index": "pypi", - "version": "==7.4.3" + "version": "==7.4.4" + }, + "pytest-cov": { + "hashes": [ + "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6", + "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a" + ], + "index": "pypi", + "version": "==4.1.0" } } } diff --git a/lint-workflow/Session.vim b/lint-workflow/Session.vim index 6ad2b247..2d3bdb42 100644 --- a/lint-workflow/Session.vim +++ b/lint-workflow/Session.vim @@ -14,25 +14,50 @@ else set shortmess=aoO endif badd +1 README.md -badd +211 cli.py -badd +1 src/load.py +badd +1 cli.py +badd +4 src/load.py badd +1 config.yaml badd +1 src/rules/__init__.py -badd +13 src/rules/name_capitalized.py -badd +11 src/rules/name_exists.py +badd +16 src/rules/name_capitalized.py +badd +17 src/rules/name_exists.py badd +1 src/rule.py -badd +0 tests/test_rule.py -badd +35 tests/rules/test_name_exists.py -badd +35 tests/rules/test_name_capitalized.py +badd +72 tests/test_rule.py +badd +40 tests/rules/test_name_exists.py +badd +2 tests/rules/test_name_capitalized.py badd +4 rule_settings.py badd +3 settings.py -badd +0 tests/fixtures/test.yml +badd +1 tests/fixtures/test.yml +badd +1 tests/fixtures/test-min-incorrect.yaml +badd +10 tests/test_load.py +badd +2 src/rules/runs_on_pinned.py +badd +3 src/rules/pinned_workflow_runner.py +badd +3 tests/rules/test_pinned_workflow_runner.py +badd +15 src/models/workflow.py +badd +9 src/rules/pinned_job_runner.py +badd +9 tests/rules/test_pinned_job_runner.py +badd +3 src/rules/job_environment_prefix.py +badd +14 src/models/job.py +badd +4 tests/rules/test_job_environment_prefix.py +badd +19 src/rules/step_hex_length.py +badd +43 tests/rules/test_step_hex_length.py +badd +5 src/rules/step_hex.py +badd +9 tests/rules/test_step_hex.py +badd +26 src/rules/step_approved.py +badd +59 tests/rules/test_step_approved.py +badd +0 src/utils.py +badd +28 src/rules/step_pinned.py +badd +82 tests/rules/test_step_pinned.py +badd +0 tests/fixtures/test-min.yaml +badd +0 actions.json +badd +0 src/models/step.py +badd +0 tests/test_utils.py argglobal %argdel $argadd README.md set stal=2 tabnew +setlocal\ bufhidden=wipe tabnew +setlocal\ bufhidden=wipe +tabnew +setlocal\ bufhidden=wipe tabrewind edit README.md let s:save_splitbelow = &splitbelow @@ -40,7 +65,10 @@ let s:save_splitright = &splitright set splitbelow splitright wincmd _ | wincmd | vsplit -1wincmd h +wincmd _ | wincmd | +vsplit +2wincmd h +wincmd w wincmd w let &splitbelow = s:save_splitbelow let &splitright = s:save_splitright @@ -51,8 +79,9 @@ set winminheight=0 set winheight=1 set winminwidth=0 set winwidth=1 -exe 'vert 1resize ' . ((&columns * 163 + 163) / 327) -exe 'vert 2resize ' . ((&columns * 163 + 163) / 327) +exe 'vert 1resize ' . ((&columns * 108 + 163) / 327) +exe 'vert 2resize ' . ((&columns * 108 + 163) / 327) +exe 'vert 3resize ' . ((&columns * 109 + 163) / 327) argglobal balt tests/fixtures/test.yml setlocal fdm=manual @@ -65,19 +94,42 @@ setlocal fdn=20 setlocal fen silent! normal! zE let &fdl = &fdl -let s:l = 10 - ((9 * winheight(0) + 32) / 65) +let s:l = 61 - ((57 * winheight(0) + 32) / 65) if s:l < 1 | let s:l = 1 | endif keepjumps exe s:l normal! zt -keepjumps 10 -normal! 0 +keepjumps 61 +normal! 04| wincmd w argglobal if bufexists(fnamemodify("settings.py", ":p")) | buffer settings.py | else | edit settings.py | endif if &buftype ==# 'terminal' silent file settings.py endif -balt cli.py +balt actions.json +setlocal fdm=manual +setlocal fde=0 +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=0 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +silent! normal! zE +let &fdl = &fdl +let s:l = 6 - ((5 * winheight(0) + 32) / 65) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 6 +normal! 019| +wincmd w +argglobal +if bufexists(fnamemodify("actions.json", ":p")) | buffer actions.json | else | edit actions.json | endif +if &buftype ==# 'terminal' + silent file actions.json +endif +balt settings.py setlocal fdm=manual setlocal fde=0 setlocal fmr={{{,}}} @@ -88,15 +140,16 @@ setlocal fdn=20 setlocal fen silent! normal! zE let &fdl = &fdl -let s:l = 2 - ((1 * winheight(0) + 32) / 65) +let s:l = 6 - ((5 * winheight(0) + 32) / 65) if s:l < 1 | let s:l = 1 | endif keepjumps exe s:l normal! zt -keepjumps 2 -normal! 06| +keepjumps 6 +normal! 068| wincmd w -exe 'vert 1resize ' . ((&columns * 163 + 163) / 327) -exe 'vert 2resize ' . ((&columns * 163 + 163) / 327) +exe 'vert 1resize ' . ((&columns * 108 + 163) / 327) +exe 'vert 2resize ' . ((&columns * 108 + 163) / 327) +exe 'vert 3resize ' . ((&columns * 109 + 163) / 327) tabnext edit cli.py let s:save_splitbelow = &splitbelow @@ -104,7 +157,10 @@ let s:save_splitright = &splitright set splitbelow splitright wincmd _ | wincmd | vsplit -1wincmd h +wincmd _ | wincmd | +vsplit +2wincmd h +wincmd w wincmd w let &splitbelow = s:save_splitbelow let &splitright = s:save_splitright @@ -115,8 +171,9 @@ set winminheight=0 set winheight=1 set winminwidth=0 set winwidth=1 -exe 'vert 1resize ' . ((&columns * 163 + 163) / 327) -exe 'vert 2resize ' . ((&columns * 163 + 163) / 327) +exe 'vert 1resize ' . ((&columns * 108 + 163) / 327) +exe 'vert 2resize ' . ((&columns * 108 + 163) / 327) +exe 'vert 3resize ' . ((&columns * 109 + 163) / 327) argglobal balt settings.py setlocal fdm=manual @@ -129,19 +186,42 @@ setlocal fdn=20 setlocal fen silent! normal! zE let &fdl = &fdl -let s:l = 3 - ((2 * winheight(0) + 32) / 65) +let s:l = 21 - ((20 * winheight(0) + 32) / 65) if s:l < 1 | let s:l = 1 | endif keepjumps exe s:l normal! zt -keepjumps 3 +keepjumps 21 normal! 0 wincmd w argglobal -if bufexists(fnamemodify("src/load.py", ":p")) | buffer src/load.py | else | edit src/load.py | endif +if bufexists(fnamemodify("src/utils.py", ":p")) | buffer src/utils.py | else | edit src/utils.py | endif if &buftype ==# 'terminal' - silent file src/load.py + silent file src/utils.py endif -balt settings.py +balt tests/test_utils.py +setlocal fdm=manual +setlocal fde=0 +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=0 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +silent! normal! zE +let &fdl = &fdl +let s:l = 25 - ((24 * winheight(0) + 32) / 65) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 25 +normal! 035| +wincmd w +argglobal +if bufexists(fnamemodify("tests/test_utils.py", ":p")) | buffer tests/test_utils.py | else | edit tests/test_utils.py | endif +if &buftype ==# 'terminal' + silent file tests/test_utils.py +endif +balt tests/test_load.py setlocal fdm=manual setlocal fde=0 setlocal fmr={{{,}}} @@ -152,24 +232,27 @@ setlocal fdn=20 setlocal fen silent! normal! zE let &fdl = &fdl -let s:l = 34 - ((21 * winheight(0) + 32) / 65) +let s:l = 1 - ((0 * winheight(0) + 32) / 65) if s:l < 1 | let s:l = 1 | endif keepjumps exe s:l normal! zt -keepjumps 34 +keepjumps 1 normal! 0 wincmd w -2wincmd w -exe 'vert 1resize ' . ((&columns * 163 + 163) / 327) -exe 'vert 2resize ' . ((&columns * 163 + 163) / 327) +exe 'vert 1resize ' . ((&columns * 108 + 163) / 327) +exe 'vert 2resize ' . ((&columns * 108 + 163) / 327) +exe 'vert 3resize ' . ((&columns * 109 + 163) / 327) tabnext -edit src/rule.py +edit src/models/job.py let s:save_splitbelow = &splitbelow let s:save_splitright = &splitright set splitbelow splitright wincmd _ | wincmd | vsplit -1wincmd h +wincmd _ | wincmd | +vsplit +2wincmd h +wincmd w wincmd w let &splitbelow = s:save_splitbelow let &splitright = s:save_splitright @@ -180,9 +263,33 @@ set winminheight=0 set winheight=1 set winminwidth=0 set winwidth=1 -exe 'vert 1resize ' . ((&columns * 163 + 163) / 327) -exe 'vert 2resize ' . ((&columns * 163 + 163) / 327) +exe 'vert 1resize ' . ((&columns * 108 + 163) / 327) +exe 'vert 2resize ' . ((&columns * 108 + 163) / 327) +exe 'vert 3resize ' . ((&columns * 109 + 163) / 327) +argglobal +balt src/rule.py +setlocal fdm=manual +setlocal fde=0 +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=0 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +silent! normal! zE +let &fdl = &fdl +let s:l = 18 - ((17 * winheight(0) + 32) / 65) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 18 +normal! 016| +wincmd w argglobal +if bufexists(fnamemodify("src/rule.py", ":p")) | buffer src/rule.py | else | edit src/rule.py | endif +if &buftype ==# 'terminal' + silent file src/rule.py +endif balt tests/test_rule.py setlocal fdm=manual setlocal fde=0 @@ -194,19 +301,111 @@ setlocal fdn=20 setlocal fen silent! normal! zE let &fdl = &fdl -let s:l = 32 - ((31 * winheight(0) + 32) / 65) +let s:l = 19 - ((6 * winheight(0) + 32) / 65) if s:l < 1 | let s:l = 1 | endif keepjumps exe s:l normal! zt -keepjumps 32 -normal! 017| +keepjumps 19 +normal! 0 wincmd w argglobal if bufexists(fnamemodify("tests/test_rule.py", ":p")) | buffer tests/test_rule.py | else | edit tests/test_rule.py | endif if &buftype ==# 'terminal' silent file tests/test_rule.py endif -balt src/rule.py +balt tests/fixtures/test-min-incorrect.yaml +setlocal fdm=manual +setlocal fde=0 +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=0 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +silent! normal! zE +let &fdl = &fdl +let s:l = 48 - ((30 * winheight(0) + 32) / 65) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 48 +normal! 09| +wincmd w +exe 'vert 1resize ' . ((&columns * 108 + 163) / 327) +exe 'vert 2resize ' . ((&columns * 108 + 163) / 327) +exe 'vert 3resize ' . ((&columns * 109 + 163) / 327) +tabnext +edit src/models/step.py +let s:save_splitbelow = &splitbelow +let s:save_splitright = &splitright +set splitbelow splitright +wincmd _ | wincmd | +vsplit +wincmd _ | wincmd | +vsplit +2wincmd h +wincmd w +wincmd w +let &splitbelow = s:save_splitbelow +let &splitright = s:save_splitright +wincmd t +let s:save_winminheight = &winminheight +let s:save_winminwidth = &winminwidth +set winminheight=0 +set winheight=1 +set winminwidth=0 +set winwidth=1 +exe 'vert 1resize ' . ((&columns * 109 + 163) / 327) +exe 'vert 2resize ' . ((&columns * 108 + 163) / 327) +exe 'vert 3resize ' . ((&columns * 108 + 163) / 327) +argglobal +balt src/rules/step_approved.py +setlocal fdm=manual +setlocal fde=0 +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=0 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +silent! normal! zE +let &fdl = &fdl +let s:l = 33 - ((32 * winheight(0) + 32) / 65) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 33 +normal! 044| +wincmd w +argglobal +if bufexists(fnamemodify("src/rules/step_approved.py", ":p")) | buffer src/rules/step_approved.py | else | edit src/rules/step_approved.py | endif +if &buftype ==# 'terminal' + silent file src/rules/step_approved.py +endif +balt src/models/step.py +setlocal fdm=manual +setlocal fde=0 +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=0 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +silent! normal! zE +let &fdl = &fdl +let s:l = 43 - ((42 * winheight(0) + 32) / 65) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 43 +normal! 018| +wincmd w +argglobal +if bufexists(fnamemodify("tests/rules/test_step_approved.py", ":p")) | buffer tests/rules/test_step_approved.py | else | edit tests/rules/test_step_approved.py | endif +if &buftype ==# 'terminal' + silent file tests/rules/test_step_approved.py +endif +balt src/rules/step_approved.py setlocal fdm=manual setlocal fde=0 setlocal fmr={{{,}}} @@ -217,16 +416,17 @@ setlocal fdn=20 setlocal fen silent! normal! zE let &fdl = &fdl -let s:l = 82 - ((64 * winheight(0) + 32) / 65) +let s:l = 63 - ((35 * winheight(0) + 32) / 65) if s:l < 1 | let s:l = 1 | endif keepjumps exe s:l normal! zt -keepjumps 82 -normal! 053| +keepjumps 63 +normal! 029| wincmd w -exe 'vert 1resize ' . ((&columns * 163 + 163) / 327) -exe 'vert 2resize ' . ((&columns * 163 + 163) / 327) -tabnext 2 +exe 'vert 1resize ' . ((&columns * 109 + 163) / 327) +exe 'vert 2resize ' . ((&columns * 108 + 163) / 327) +exe 'vert 3resize ' . ((&columns * 108 + 163) / 327) +tabnext 1 set stal=1 if exists('s:wipebuf') && len(win_findbuf(s:wipebuf)) == 0 && getbufvar(s:wipebuf, '&buftype') isnot# 'terminal' silent exe 'bwipe ' . s:wipebuf diff --git a/lint-workflow/Taskfile.yml b/lint-workflow/Taskfile.yml new file mode 100644 index 00000000..83315751 --- /dev/null +++ b/lint-workflow/Taskfile.yml @@ -0,0 +1,29 @@ +# https://taskfile.dev + +version: '3' + +tasks: + fmt: + silent: true + cmds: + - pipenv run black . + + test:unit: + cmds: + - pipenv run pytest tests + + test:unit:cov: + cmds: + - pipenv run pytest tests --cov=src + + test:e2e:lint: + cmds: + - pipenv run python cli.py -v lint --files tests/fixtures/test.yml + + test:e2e:actions:add: + cmds: + - pipenv run python cli.py -v actions --output test.json add bitwarden/sm-action + + test:e2e:actions:update: + cmds: + - pipenv run python cli.py -v actions update --output test.json diff --git a/lint-workflow/actions.json b/lint-workflow/actions.json index 2c326e29..f601cb89 100644 --- a/lint-workflow/actions.json +++ b/lint-workflow/actions.json @@ -1,54 +1,262 @@ -[ - {"name": "act10ns/slack", "version": "", "sha": ""}, - {"name": "actions/cache", "version": "", "sha": ""}, - {"name": "actions/checkout", "version": "", "sha": ""}, - {"name": "actions/delete-package-versions", "version": "", "sha": ""}, - {"name": "actions/download-artifact", "version": "", "sha": ""}, - {"name": "actions/github-script", "version": "", "sha": ""}, - {"name": "actions/labeler", "version": "", "sha": ""}, - {"name": "actions/setup-dotnet", "version": "", "sha": ""}, - {"name": "actions/setup-java", "version": "", "sha": ""}, - {"name": "actions/setup-node", "version": "", "sha": ""}, - {"name": "actions/setup-python", "version": "", "sha": ""}, - {"name": "actions/stale", "version": "", "sha": ""}, - {"name": "actions/upload-artifact", "version": "", "sha": ""}, - {"name": "android-actions/setup-android", "version": "", "sha": ""}, - {"name": "Asana/create-app-attachment-github-action", "version": "", "sha": ""}, - {"name": "Azure/functions-action", "version": "", "sha": ""}, - {"name": "Azure/get-keyvault-secrets", "version": "", "sha": ""}, - {"name": "Azure/login", "version": "", "sha": ""}, - {"name": "azure/webapps-deploy", "version": "", "sha": ""}, - {"name": "bitwarden/sm-action", "version": "", "sha": ""}, - {"name": "checkmarx/ast-github-action", "version": "", "sha": ""}, - {"name": "chrnorm/deployment-action", "version": "", "sha": ""}, - {"name": "chrnorm/deployment-status", "version": "", "sha": ""}, - {"name": "chromaui/action", "version": "", "sha": ""}, - {"name": "cloudflare/pages-action", "version": "", "sha": ""}, - {"name": "convictional/trigger-workflow-and-wait", "version": "", "sha": ""}, - {"name": "crazy-max/ghaction-import-gpg", "version": "", "sha": ""}, - {"name": "crowdin/github-action", "version": "", "sha": ""}, - {"name": "dawidd6/action-download-artifact", "version": "", "sha": ""}, - {"name": "dawidd6/action-homebrew-bump-formula", "version": "", "sha": ""}, - {"name": "digitalocean/action-doctl", "version": "", "sha": ""}, - {"name": "docker/build-push-action", "version": "", "sha": ""}, - {"name": "docker/setup-buildx-action", "version": "", "sha": ""}, - {"name": "docker/setup-qemu-action", "version": "", "sha": ""}, - {"name": "dorny/test-reporter", "version": "", "sha": ""}, - {"name": "dtolnay/rust-toolchain", "version": "", "sha": ""}, - {"name": "futureware-tech/simulator-action", "version": "", "sha": ""}, - {"name": "hashicorp/setup-packer", "version": "", "sha": ""}, - {"name": "macauley/action-homebrew-bump-cask", "version": "", "sha": ""}, - {"name": "microsoft/setup-msbuild", "version": "", "sha": ""}, - {"name": "ncipollo/release-action", "version": "", "sha": ""}, - {"name": "peter-evans/close-issue", "version": "", "sha": ""}, - {"name": "ruby/setup-ruby", "version": "", "sha": ""}, - {"name": "samuelmeuli/action-snapcraft", "version": "", "sha": ""}, - {"name": "snapcore/action-build", "version": "", "sha": ""}, - {"name": "sonarsource/sonarcloud-github-action", "version": "", "sha": ""}, - {"name": "stackrox/kube-linter-action", "version": "", "sha": ""}, - {"name": "Swatinem/rust-cache", "version": "", "sha": ""}, - {"name": "SwiftDocOrg/github-wiki-publish-action", "version": "", "sha": ""}, - {"name": "SwiftDocOrg/swift-doc", "version": "", "sha": ""}, - {"name": "tj-actions/changed-files", "version": "", "sha": ""}, - {"name": "yogevbd/enforce-label-action", "version": "", "sha": ""} -] +{ + "Asana/create-app-attachment-github-action": { + "name": "Asana/create-app-attachment-github-action", + "sha": "affc72d57bac733d864d4189ed69a9cbd61a9e4f", + "version": "v1.3" + }, + "Azure/functions-action": { + "name": "Azure/functions-action", + "sha": "238dc3c45bb1b04e5d16ff9e75cddd1d86753bd6", + "version": "v1.5.1" + }, + "Azure/get-keyvault-secrets": { + "name": "Azure/get-keyvault-secrets", + "sha": "b5c723b9ac7870c022b8c35befe620b7009b336f", + "version": "v1" + }, + "Azure/login": { + "name": "Azure/login", + "sha": "de95379fe4dadc2defb305917eaa7e5dde727294", + "version": "v1.5.1" + }, + "Swatinem/rust-cache": { + "name": "Swatinem/rust-cache", + "sha": "a95ba195448af2da9b00fb742d14ffaaf3c21f43", + "version": "v2.7.0" + }, + "SwiftDocOrg/github-wiki-publish-action": { + "name": "SwiftDocOrg/github-wiki-publish-action", + "sha": "a87db85ed06e4431be29cfdcb22b9653881305d0", + "version": "1.0.0" + }, + "SwiftDocOrg/swift-doc": { + "name": "SwiftDocOrg/swift-doc", + "sha": "f935ebfe524a0ff27bda07dadc3662e3e45b5125", + "version": "1.0.0-rc.1" + }, + "act10ns/slack": { + "name": "act10ns/slack", + "sha": "ed1309ab9862e57e9e583e51c7889486b9a00b0f", + "version": "v2.0.0" + }, + "actions/cache": { + "name": "actions/cache", + "sha": "704facf57e6136b1bc63b828d79edcd491f0ee84", + "version": "v3.3.2" + }, + "actions/checkout": { + "name": "actions/checkout", + "sha": "b4ffde65f46336ab88eb53be808477a3936bae11", + "version": "v4.1.1" + }, + "actions/delete-package-versions": { + "name": "actions/delete-package-versions", + "sha": "0d39a63126868f5eefaa47169615edd3c0f61e20", + "version": "v4.1.1" + }, + "actions/download-artifact": { + "name": "actions/download-artifact", + "sha": "f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110", + "version": "v4.1.0" + }, + "actions/github-script": { + "name": "actions/github-script", + "sha": "60a0d83039c74a4aee543508d2ffcb1c3799cdea", + "version": "v7.0.1" + }, + "actions/labeler": { + "name": "actions/labeler", + "sha": "8558fd74291d67161a8a78ce36a881fa63b766a9", + "version": "v5.0.0" + }, + "actions/setup-dotnet": { + "name": "actions/setup-dotnet", + "sha": "4d6c8fcf3c8f7a60068d26b594648e99df24cee3", + "version": "v4.0.0" + }, + "actions/setup-java": { + "name": "actions/setup-java", + "sha": "387ac29b308b003ca37ba93a6cab5eb57c8f5f93", + "version": "v4.0.0" + }, + "actions/setup-node": { + "name": "actions/setup-node", + "sha": "b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8", + "version": "v4.0.1" + }, + "actions/setup-python": { + "name": "actions/setup-python", + "sha": "0a5c61591373683505ea898e09a3ea4f39ef2b9c", + "version": "v5.0.0" + }, + "actions/stale": { + "name": "actions/stale", + "sha": "28ca1036281a5e5922ead5184a1bbf96e5fc984e", + "version": "v9.0.0" + }, + "actions/upload-artifact": { + "name": "actions/upload-artifact", + "sha": "c7d193f32edcb7bfad88892161225aeda64e9392", + "version": "v4.0.0" + }, + "android-actions/setup-android": { + "name": "android-actions/setup-android", + "sha": "07976c6290703d34c16d382cb36445f98bb43b1f", + "version": "v3.2.0" + }, + "azure/webapps-deploy": { + "name": "azure/webapps-deploy", + "sha": "145a0687697df1d8a28909569f6e5d86213041f9", + "version": "v3.0.0" + }, + "bitwarden/sm-action": { + "name": "bitwarden/sm-action", + "sha": "92d1d6a4f26a89a8191c83ab531a53544578f182", + "version": "v2.0.0" + }, + "checkmarx/ast-github-action": { + "name": "checkmarx/ast-github-action", + "sha": "72d549beebd0bc5bbafa559f198161b6ce7c03df", + "version": "2.0.21" + }, + "chrnorm/deployment-action": { + "name": "chrnorm/deployment-action", + "sha": "d42cde7132fcec920de534fffc3be83794335c00", + "version": "v2.0.5" + }, + "chrnorm/deployment-status": { + "name": "chrnorm/deployment-status", + "sha": "2afb7d27101260f4a764219439564d954d10b5b0", + "version": "v2.0.1" + }, + "chromaui/action": { + "name": "chromaui/action", + "sha": "80bf5911f28005ed208f15b7268843b79ca0e23a", + "version": "v1" + }, + "cloudflare/pages-action": { + "name": "cloudflare/pages-action", + "sha": "f0a1cd58cd66095dee69bfa18fa5efd1dde93bca", + "version": "v1.5.0" + }, + "convictional/trigger-workflow-and-wait": { + "name": "convictional/trigger-workflow-and-wait", + "sha": "f69fa9eedd3c62a599220f4d5745230e237904be", + "version": "v1.6.5" + }, + "crazy-max/ghaction-import-gpg": { + "name": "crazy-max/ghaction-import-gpg", + "sha": "01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4", + "version": "v6.1.0" + }, + "crowdin/github-action": { + "name": "crowdin/github-action", + "sha": "fdc55cdc519e86e32c22a07528d649277f1127f2", + "version": "v1.16.0" + }, + "dawidd6/action-download-artifact": { + "name": "dawidd6/action-download-artifact", + "sha": "e7466d1a7587ed14867642c2ca74b5bcc1e19a2d", + "version": "v3.0.0" + }, + "dawidd6/action-homebrew-bump-formula": { + "name": "dawidd6/action-homebrew-bump-formula", + "sha": "75ed025ff3ad1d617862838b342b06d613a0ddf3", + "version": "v3.10.1" + }, + "digitalocean/action-doctl": { + "name": "digitalocean/action-doctl", + "sha": "e5cb5b0cde9789f79c5115c2c4d902f38a708804", + "version": "v2.5.0" + }, + "docker/build-push-action": { + "name": "docker/build-push-action", + "sha": "4a13e500e55cf31b7a5d59a38ab2040ab0f42f56", + "version": "v5.1.0" + }, + "docker/setup-buildx-action": { + "name": "docker/setup-buildx-action", + "sha": "f95db51fddba0c2d1ec667646a06c2ce06100226", + "version": "v3.0.0" + }, + "docker/setup-qemu-action": { + "name": "docker/setup-qemu-action", + "sha": "68827325e0b33c7199eb31dd4e31fbe9023e06e3", + "version": "v3.0.0" + }, + "dorny/test-reporter": { + "name": "dorny/test-reporter", + "sha": "afe6793191b75b608954023a46831a3fe10048d4", + "version": "v1.7.0" + }, + "dtolnay/rust-toolchain": { + "name": "dtolnay/rust-toolchain", + "sha": "1482605bfc5719782e1267fd0c0cc350fe7646b8", + "version": "v1" + }, + "futureware-tech/simulator-action": { + "name": "futureware-tech/simulator-action", + "sha": "bfa03d93ec9de6dacb0c5553bbf8da8afc6c2ee9", + "version": "v3" + }, + "hashicorp/setup-packer": { + "name": "hashicorp/setup-packer", + "sha": "ecc5516821087666a672c0d280a0084ea6d9aafd", + "version": "v2.0.1" + }, + "macauley/action-homebrew-bump-cask": { + "name": "macauley/action-homebrew-bump-cask", + "sha": "445c42390d790569d938f9068d01af39ca030feb", + "version": "v1.0.0" + }, + "microsoft/setup-msbuild": { + "name": "microsoft/setup-msbuild", + "sha": "1ff57057b5cfdc39105cd07a01d78e9b0ea0c14c", + "version": "v1.3.1" + }, + "ncipollo/release-action": { + "name": "ncipollo/release-action", + "sha": "6c75be85e571768fa31b40abf38de58ba0397db5", + "version": "v1.13.0" + }, + "peter-evans/close-issue": { + "name": "peter-evans/close-issue", + "sha": "276d7966e389d888f011539a86c8920025ea0626", + "version": "v3.0.1" + }, + "ruby/setup-ruby": { + "name": "ruby/setup-ruby", + "sha": "360dc864d5da99d54fcb8e9148c14a84b90d3e88", + "version": "v1.165.1" + }, + "samuelmeuli/action-snapcraft": { + "name": "samuelmeuli/action-snapcraft", + "sha": "d33c176a9b784876d966f80fb1b461808edc0641", + "version": "v2.1.1" + }, + "snapcore/action-build": { + "name": "snapcore/action-build", + "sha": "2096990827aa966f773676c8a53793c723b6b40f", + "version": "v1.2.0" + }, + "sonarsource/sonarcloud-github-action": { + "name": "sonarsource/sonarcloud-github-action", + "sha": "49e6cd3b187936a73b8280d59ffd9da69df63ec9", + "version": "v2.1.1" + }, + "stackrox/kube-linter-action": { + "name": "stackrox/kube-linter-action", + "sha": "ca0d55b925470deb5b04b556e6c4276ea94d03c3", + "version": "v1.0.4" + }, + "tj-actions/changed-files": { + "name": "tj-actions/changed-files", + "sha": "716b1e13042866565e00e85fd4ec490e186c4a2f", + "version": "v41.0.1" + }, + "yogevbd/enforce-label-action": { + "name": "yogevbd/enforce-label-action", + "sha": "a3c219da6b8fa73f6ba62b68ff09c469b3a1c024", + "version": "2.2.2" + } +} diff --git a/lint-workflow/cli.py b/lint-workflow/cli.py index 01d0179a..b5d995c5 100644 --- a/lint-workflow/cli.py +++ b/lint-workflow/cli.py @@ -3,26 +3,17 @@ import os import yaml import json -import urllib3 as urllib -import logging # from src.rules import workflow_rules, job_rules, step_rules, uses_step_rules, run_step_rules import settings -from src.load import WorkflowBuilder, Rules -from src.utils import LintFinding, Settings, SettingsError +from src.actions import Actions +from src.utils import Settings, SettingsError +from src.lint import Linter -PROBLEM_LEVELS = { - "warning": 1, - "error": 2, -} - -memoized_action_update_urls = {} - try: lint_settings = Settings( - enabled_rules=settings.enabled_rules, - approved_actions=settings.approved_actions + enabled_rules=settings.enabled_rules, approved_actions=settings.approved_actions ) except: raise SettingsError( @@ -32,439 +23,72 @@ ) ) -lint_rules = Rules(settings=lint_settings, verbose=True) +linter = Linter(settings=lint_settings) +actions = Actions(settings=lint_settings) # print(lint_rules.workflow) -class Colors: - """Class containing color codes for printing strings to output.""" - - black = "30m" - red = "31m" - green = "32m" - yellow = "33m" - blue = "34m" - magenta = "35m" - cyan = "36m" - white = "37m" - - -def get_max_error_level(findings): - """Get max error level from list of findings.""" - if len(findings) == 0: - return 0 - max_problem = max(findings, key=lambda finding: PROBLEM_LEVELS[finding.level]) - max_problem_level = PROBLEM_LEVELS[max_problem.level] - return max_problem_level - - -def print_finding(finding: LintFinding): - """Print formatted and colored finding.""" - if finding.level == "warning": - color = Colors.yellow - elif finding.level == "error": - color = Colors.red - else: - color = Colors.white - - line = f" - \033[{color}{finding.level}\033[0m {finding.description}" - - print(line) - - -def get_github_api_response(url, action_id): - """Call GitHub API with error logging without throwing an exception.""" - http = urllib.PoolManager() - headers = {"user-agent": "bw-linter"} - - if os.getenv("GITHUB_TOKEN", None): - headers["Authorization"] = f"Token {os.environ['GITHUB_TOKEN']}" - - response = http.request("GET", url, headers=headers) - - if response.status == 403 and response.reason == "rate limit exceeded": - logging.error( - f"Failed to call GitHub API for action: {action_id} due to rate limit exceeded." - ) - return None - - if response.status == 401 and response.reason == "Unauthorized": - logging.error( - f"Failed to call GitHub API for action: {action_id}: {response.data}." - ) - return None - - return response - - -def action_repo_exists(action_id): - """ - Takes and action id and checks if the action repo exists. - - Example action_id: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945 - """ - - if "./" in action_id: - # Handle local workflow calls, return None since there will be no updates. - return True - - path, *hash = action_id.split("@") - - if "bitwarden" in path: - path_list = path.split("/", 2) - url = f"https://api.github.com/repos/{path_list[0]}/{path_list[1]}" - response = get_github_api_response(url, action_id) - - else: - response = get_github_api_response( - f"https://api.github.com/repos/{path}", action_id - ) - - if response is None: - # Handle github api limit exceed by returning that the action exists without actually checking - # to prevent false errors on linter output. Only show it as an linter error. - return True - - if response.status == 404: - return False - - return True - - -def workflow_files(input: str) -> list: - """ - Takes in an argument of directory and/or files in string format from the CLI. - Returns a sorted set of all workflow files in the path(s) specified. - """ - workflow_files = [] - for path in input.split(): - if os.path.isfile(path): - workflow_files.append(path) - elif os.path.isdir(path): - for subdir, dirs, files in os.walk(path): - for filename in files: - filepath = subdir + os.sep + filename - if filepath.endswith((".yml", ".yaml")): - workflow_files.append(filepath) - - return sorted(set(workflow_files)) - - -def get_action_update(action_id): - """ - Takes in an action id (bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945) - and checks the action repo for the newest version. - If there is a new version, return the url to the updated version. - """ - if "./" in action_id: - # Handle local workflow calls, return None since there will be no updates. - return None - - path, *hash = action_id.split("@") - - if path in memoized_action_update_urls: - return memoized_action_update_urls[path] - else: - if "bitwarden" in path: - path_list = path.split("/", 2) - url = f"https://api.github.com/repos/{path_list[0]}/{path_list[1]}/commits?path={path_list[2]}" - response = get_github_api_response(url, action_id) - if not response: - return None - - sha = json.loads(response.data)[0]["sha"] - if sha not in hash: - update_url = ( - f"https://github.com/{path_list[0]}/{path_list[1]}/commit/{sha}" - ) - memoized_action_update_urls[path] = update_url - return update_url - else: - # Get tag from latest release - response = get_github_api_response( - f"https://api.github.com/repos/{path}/releases/latest", action_id - ) - if not response: - return None - - tag_name = json.loads(response.data)["tag_name"] - - # Get the URL to the commit for the tag - response = get_github_api_response( - f"https://api.github.com/repos/{path}/git/ref/tags/{tag_name}", - action_id, - ) - if not response: - return None - - if json.loads(response.data)["object"]["type"] == "commit": - sha = json.loads(response.data)["object"]["sha"] - else: - url = json.loads(response.data)["object"]["url"] - # Follow the URL and get the commit sha for tags - response = get_github_api_response(url, action_id) - if not response: - return None - - sha = json.loads(response.data)["object"]["sha"] - - if sha not in hash: - update_url = f"https://github.com/{path}/commit/{sha}" - memoized_action_update_urls[path] = update_url - return update_url - - -def _new_lint(filename): - findings = [] - max_error_level = 0 - - print(f"Linting: {filename}") - with open(filename) as file: - workflow = WorkflowBuilder.build(filename) - - for rule in lint_rules.workflow: - findings.append(rule.execute(workflow)) - - for job_key, job in workflow.jobs.items(): - for rule in lint_rules.job: - findings.append(rule.execute(job)) - - for step in job.steps: - for rule in lint_rules.step: - findings.append(rule.execute(step)) - - findings = list(filter(lambda a: a is not None, findings)) - - if len(findings) > 0: - print("#", filename) - for finding in findings: - print_finding(finding) - print() - - max_error_level = get_max_error_level(findings) - - return max_error_level - - -def lint(filename): - return _new_lint(filename) - - -def _old_lint(filename): - findings = [] - max_error_level = 0 - - with open(filename) as file: - workflow = yaml.load(file, Loader=yaml.FullLoader) - - # Check for 'name' key for the workflow. - if "name" not in workflow: - findings.append(LintFinding("Name key missing for workflow.", "warning")) - - # Check for 'name' value to be capitalized in workflow. - elif not workflow["name"][0].isupper(): - findings.append( - LintFinding( - f"Name value for workflow is not capitalized. [{workflow['name']}]", - "warning", - ) - ) - - # Loop through jobs in workflow. - if "jobs" in workflow: - jobs = workflow["jobs"] - for job_key in jobs: - job = jobs[job_key] - - # Make sure runner is using pinned version. - runner = job.get("runs-on", "") - if "-latest" in runner: - findings.append( - LintFinding( - f"Runner version is set to '{runner}', but needs to be pinned to a version.", - "warning", - ) - ) - - # Check for 'name' key for job. - if "name" not in job: - findings.append( - LintFinding( - f"Name key missing for job key '{job_key}'.", "warning" - ) - ) - # Check for 'name' value to be capitalized in job. - elif not job["name"][0].isupper(): - findings.append( - LintFinding( - f"Name value of job key '{job_key}' is not capitalized. [{job['name']}]", - "warning", - ) - ) - - # If the job has environment variables defined, then make sure they start with an underscore. - if "env" in job: - for k in job["env"].keys(): - if k[0] != "_": - findings.append( - LintFinding( - f"Environment variable '{k}' of job key '{job_key}' does not start with an underscore.", - "warning", - ) - ) - - # Loop through steps in job. - steps = job.get("steps", "") - for i, step in enumerate(steps, start=1): - # Check for 'name' key for step. - if "name" not in step: - findings.append( - LintFinding( - f"Name key missing for step {str(i)} of job key '{job_key}'.", - "warning", - ) - ) - # Check for 'name' value to be capitalized in step. - elif not step["name"][0].isupper(): - findings.append( - LintFinding( - f"Name value in step {str(i)} of job key '{job_key}' is not capitalized. [{step['name']}]", - "warning", - ) - ) - - if "uses" in step: - try: - path, hash = step["uses"].split("@") - except ValueError: - logging.info("Skipping local action in workflow.") - break - - # If the step has a 'uses' key, check value hash. - try: - # Check to make sure SHA1 hash is 40 characters. - if len(hash) != 40: - findings.append( - LintFinding( - f"Step {str(i)} of job key '{job_key}' does not have a valid action hash. (not 40 characters)", - "error", - ) - ) - - # Attempts to convert the hash to a integer - # which will succeed if all characters are hexadecimal - try: - int(hash, 16) - except ValueError: - findings.append( - LintFinding( - f"Step {str(i)} of job key '{job_key}' does not have a valid action hash. (not all hexadecimal characters)", - "error", - ) - ) - except: - findings.append( - LintFinding( - f"Step {str(i)} of job key '{job_key}' does not have a valid action hash. (missing '@' character)", - "error", - ) - ) - - # If the step has a 'uses' key, check path for external workflow - path_list = path.split("/", 2) - - if "bitwarden" in path and len(path_list) < 3: - findings.append( - LintFinding( - f"Step {str(i)} of job key '{job_key}' does not have a valid action path. (missing name of the repository or workflow)", - "error", - ) - ) - elif len(path_list) < 2: - findings.append( - LintFinding( - f"Step {str(i)} of job key '{job_key}' does not have a valid action path. (missing workflow name or the workflow author)", - "error", - ) - ) - # Check if GitHub repository with action exists - elif not action_repo_exists(step["uses"]): - action_id = step["uses"] - findings.append( - LintFinding( - f"Step {str(i)} of job key '{job_key}' uses an non-existing action: {action_id}.", - "error", - ) - ) - else: - # If the step has a 'uses' key and path is correct, check the action id repo for an update. - update_available = get_action_update(step["uses"]) - if update_available: - findings.append( - LintFinding( - f"Step {str(i)} of job key '{job_key}' uses an outdated action, consider updating it '{update_available}'.", - "warning", - ) - ) - - # If the step has a 'run' key and only has one command, check if it's a single line. - if "run" in step: - if step["run"].count("\n") == 1: - findings.append( - LintFinding( - f"Run in step {str(i)} of job key '{job_key}' should be a single line.", - "error", - ) - ) - - if len(findings) > 0: - print("#", filename) - for finding in findings: - print_finding(finding) - print() - - max_error_level = get_max_error_level(findings) +def main(input_args=None): + # Read arguments from command line. + parser = argparse.ArgumentParser(prog="workflow-linter") + parser.add_argument("-v", "--verbose", action="store_true", default=False) + subparsers = parser.add_subparsers(required=True, dest="command") - return max_error_level + parser_actions = subparsers.add_parser("actions", help="actions help") + parser_actions.add_argument( + "-o", "--output", action="store", default="actions.json" + ) + subparsers_actions = parser_actions.add_subparsers( + required=True, dest="actions_command" + ) + parser_actions_update = subparsers_actions.add_parser( + "update", help="update action versions" + ) -def main(input_args=None): - # Pull the arguments from the command line - if not input_args: - input_args = sys.argv[1:] + parser_actions_add = subparsers_actions.add_parser( + "add", help="add action to approved list" + ) + parser_actions_add.add_argument("name", help="action name [git owener/repo]") - # Read arguments from command line. - parser = argparse.ArgumentParser() - parser.add_argument("input", help="file or directory input") - parser.add_argument( + parser_lint = subparsers.add_parser("lint", help="lint help") + parser_lint.add_argument( "-s", "--strict", action="store_true", - help="return non-zero exit code on warnings " "as well as errors", + help="return non-zero exit code on warnings as well as errors", + ) + parser_lint.add_argument("-f", "--files", action="append", help="files to lint") + parser_lint.add_argument( + "--output", + action="store", + help="output format: [stdout|json|md]", + default="stdout", ) - args = parser.parse_args(input_args) - input_files = workflow_files(args.input) - if len(input_files) > 0: - prob_levels = list(map(lint, input_files)) + # Pull the arguments from the command line + input_args = sys.argv[1:] + if not input_args: + raise SystemExit(parser.print_help()) + + args = parser.parse_args(input_args) - max_error_level = max(prob_levels) + if args.verbose: + print(f"Args:\n{args}") - if max_error_level == PROBLEM_LEVELS["error"]: - return_code = 2 - elif max_error_level == PROBLEM_LEVELS["warning"]: - return_code = 1 if args.strict else 0 - else: - return_code = 0 + if args.command == "lint": + return linter.run(args.files) - return return_code - else: - print(f'File(s)/Directory: "{args.input}" does not exist, exiting.') + if args.command == "actions": + if args.actions_command == "add": + return actions.add(args.name, args.output) + elif args.actions_command == "update": + return actions.update(args.output) return -1 if __name__ == "__main__": return_code = main() - print(memoized_action_update_urls) + # print(memoized_action_update_urls) sys.exit(return_code) diff --git a/lint-workflow/settings.py b/lint-workflow/settings.py index 3d1d2480..c86357e1 100644 --- a/lint-workflow/settings.py +++ b/lint-workflow/settings.py @@ -1,63 +1,15 @@ +import json +import os + + enabled_rules = [ "src.rules.name_exists.RuleNameExists", "src.rules.name_capitalized.RuleNameCapitalized", "src.rules.pinned_job_runner.RuleJobRunnerVersionPinned", "src.rules.job_environment_prefix.RuleJobEnvironmentPrefix", - "src.rules.step_hex.RuleStepUsesPinned", + "src.rules.step_pinned.RuleStepUsesPinned", ] -approved_actions = [ - "act10ns/slack", - "actions/cache", - "actions/checkout", - "actions/delete-package-versions", - "actions/download-artifact", - "actions/github-script", - "actions/labeler", - "actions/setup-dotnet", - "actions/setup-java", - "actions/setup-node", - "actions/setup-python", - "actions/stale", - "actions/upload-artifact", - "android-actions/setup-android", - "Asana/create-app-attachment-github-action", - "Azure/functions-action", - "Azure/get-keyvault-secrets", - "Azure/login", - "azure/webapps-deploy", - "bitwarden/sm-action", - "checkmarx/ast-github-action", - "chrnorm/deployment-action", - "chrnorm/deployment-status", - "chromaui/action", - "cloudflare/pages-action", - "convictional/trigger-workflow-and-wait", - "crazy-max/ghaction-import-gpg", - "crowdin/github-action", - "dawidd6/action-download-artifact", - "dawidd6/action-homebrew-bump-formula", - "digitalocean/action-doctl", - "docker/build-push-action", - "docker/setup-buildx-action", - "docker/setup-qemu-action", - "dorny/test-reporter", - "dtolnay/rust-toolchain", - "futureware-tech/simulator-action", - "hashicorp/setup-packer", - "macauley/action-homebrew-bump-cask", - "microsoft/setup-msbuild", - "ncipollo/release-action", - "peter-evans/close-issue", - "ruby/setup-ruby", - "samuelmeuli/action-snapcraft", - "snapcore/action-build", - "sonarsource/sonarcloud-github-action", - "stackrox/kube-linter-action", - "Swatinem/rust-cache", - "SwiftDocOrg/github-wiki-publish-action", - "SwiftDocOrg/swift-doc", - "tj-actions/changed-files", - "yogevbd/enforce-label-action", -] +with open("actions.json", "r") as action_file: + approved_actions = json.loads(action_file.read()) diff --git a/lint-workflow/src/actions.py b/lint-workflow/src/actions.py new file mode 100644 index 00000000..e85f6c83 --- /dev/null +++ b/lint-workflow/src/actions.py @@ -0,0 +1,132 @@ +import json +import logging +import os +import urllib3 as urllib + +from dataclasses import asdict +from typing import Union, Tuple + +from src.utils import Colors, LintFinding, Settings, SettingsError, Action + + +class Actions: + def __init__(self, settings: Settings = None) -> None: + self.settings = settings + + def get_github_api_response( + self, url: str, action_name: str + ) -> Union[urllib.response.BaseHTTPResponse, None]: + """Call GitHub API with error logging without throwing an exception.""" + + http = urllib.PoolManager() + headers = {"user-agent": "bw-linter"} + + if os.getenv("GITHUB_TOKEN", None): + headers["Authorization"] = f"Token {os.environ['GITHUB_TOKEN']}" + + response = http.request("GET", url, headers=headers) + + if response.status == 403 and response.reason == "rate limit exceeded": + logging.error( + f"Failed to call GitHub API for action: {action_name} due to rate limit exceeded." + ) + return None + + if response.status == 401 and response.reason == "Unauthorized": + logging.error( + f"Failed to call GitHub API for action: {action_name}: {response.data}." + ) + return None + + return response + + def exists(self, action: Action) -> bool: + """Takes and action id and checks if the action repo exists.""" + + url = f"https://api.github.com/repos/{action.name}" + response = self.get_github_api_response(url, action.name) + + if response is None: + # Handle github api limit exceed by returning that the action exists without actually checking + # to prevent false errors on linter output. Only show it as an linter error. + return True + + if response.status == 404: + return False + + return True + + def get_latest_version(self, action: Action) -> Tuple[str, str]: + """Gets the latest version of the Action to compare against.""" + + # Get tag from latest release + response = self.get_github_api_response( + f"https://api.github.com/repos/{action.name}/releases/latest", action.name + ) + if not response: + return None + + tag_name = json.loads(response.data)["tag_name"] + + # Get the URL to the commit for the tag + response = self.get_github_api_response( + f"https://api.github.com/repos/{action.name}/git/ref/tags/{tag_name}", + action.name, + ) + if not response: + return None + + if json.loads(response.data)["object"]["type"] == "commit": + sha = json.loads(response.data)["object"]["sha"] + else: + url = json.loads(response.data)["object"]["url"] + # Follow the URL and get the commit sha for tags + response = self.get_github_api_response(url, action.name) + if not response: + return None + + sha = json.loads(response.data)["object"]["sha"] + + return Action(name=action.name, version=tag_name, sha=sha) + + def add(self, new_action_name: str, filename: str) -> None: + print("Actions: add") + updated_actions = self.settings.approved_actions + proposed_action = Action(name=new_action_name) + + if self.exists(proposed_action): + latest = self.get_latest_version(proposed_action) + updated_actions[latest.name] = latest + + with open(filename, "w") as action_file: + converted_updated_actions = { + name: asdict(action) for name, action in updated_actions.items() + } + action_file.write( + json.dumps(converted_updated_actions, indent=2, sort_keys=True) + ) + + def update(self, filename: str) -> None: + print("Actions: update") + updated_actions = {} + for action in self.settings.approved_actions.values(): + if self.exists(action): + latest_release = self.get_latest_version(action) + if action != latest_release: + print( + ( + f" - {action.name} \033[{Colors.yellow}changed\033[0m: " + f"({action.version}, {action.sha}) => ({latest_release.version}, {latest_release.sha})" + ) + ) + else: + print(f" - {action.name} \033[{Colors.green}ok\033[0m") + updated_actions[action.name] = latest_release + + with open(filename, "w") as action_file: + converted_updated_actions = { + name: asdict(action) for name, action in updated_actions.items() + } + action_file.write( + json.dumps(converted_updated_actions, indent=2, sort_keys=True) + ) diff --git a/lint-workflow/src/lint.py b/lint-workflow/src/lint.py new file mode 100644 index 00000000..f08a85e6 --- /dev/null +++ b/lint-workflow/src/lint.py @@ -0,0 +1,104 @@ +import os + +from src.load import WorkflowBuilder, Rules +from src.utils import Colors, LintFinding, Settings, SettingsError + + +PROBLEM_LEVELS = { + "warning": 1, + "error": 2, +} + + +class Linter: + def __init__(self, settings: Settings = None, verbose: bool = True) -> None: + self.rules = Rules(settings=settings, verbose=verbose) + + def get_max_error_level(self, findings: list[LintFinding]) -> int: + """Get max error level from list of findings.""" + if len(findings) == 0: + return 0 + max_problem = max(findings, key=lambda finding: PROBLEM_LEVELS[finding.level]) + max_problem_level = PROBLEM_LEVELS[max_problem.level] + return max_problem_level + + def print_finding(self, finding: LintFinding) -> None: + """Print formatted and colored finding.""" + if finding.level == "warning": + color = Colors.yellow + elif finding.level == "error": + color = Colors.red + else: + color = Colors.white + + line = f" - \033[{color}{finding.level}\033[0m {finding.description}" + + print(line) + + def lint_file(self, filename: str) -> int: + findings = [] + max_error_level = 0 + + print(f"Linting: {filename}\n") + with open(filename) as file: + workflow = WorkflowBuilder.build(filename) + + for rule in self.rules.workflow: + findings.append(rule.execute(workflow)) + + for job_key, job in workflow.jobs.items(): + for rule in self.rules.job: + findings.append(rule.execute(job)) + + for step in job.steps: + for rule in self.rules.step: + findings.append(rule.execute(step)) + + findings = list(filter(lambda a: a is not None, findings)) + + if len(findings) > 0: + for finding in findings: + self.print_finding(finding) + print() + + max_error_level = self.get_max_error_level(findings) + + return max_error_level + + def generate_files(self, files: list) -> list: + """ + Takes in an argument of directory and/or files in list format from the CLI. + Returns a sorted set of all workflow files in the path(s) specified. + """ + workflow_files = [] + for path in files: + if os.path.isfile(path): + workflow_files.append(path) + elif os.path.isdir(path): + for subdir, dirs, files in os.walk(path): + for filename in files: + filepath = subdir + os.sep + filename + if filepath.endswith((".yml", ".yaml")): + workflow_files.append(filepath) + + return sorted(set(workflow_files)) + + def run(self, input_files: list[str]) -> int: + files = self.generate_files(input_files) + + if len(input_files) > 0: + prob_levels = list(map(self.lint_file, files)) + + max_error_level = max(prob_levels) + + if max_error_level == PROBLEM_LEVELS["error"]: + return_code = 2 + elif max_error_level == PROBLEM_LEVELS["warning"]: + return_code = 1 if args.strict else 0 + else: + return_code = 0 + + return return_code + else: + print(f'File(s)/Directory: "{input_files}" does not exist, exiting.') + return -1 diff --git a/lint-workflow/src/load.py b/lint-workflow/src/load.py index 7270a28a..7f9eed0b 100644 --- a/lint-workflow/src/load.py +++ b/lint-workflow/src/load.py @@ -80,8 +80,8 @@ def __init__(self, settings: Settings, verbose: bool = False) -> None: self.job.append(rule_inst) if Step in rule_inst.compatibility: self.step.append(rule_inst) - except Error: - print(f"Error loading: {rule}\n{Error}") + except Exception as err: + print(f"Error loading: {rule}\n{err}") if verbose: print("===== Loaded Rules =====") diff --git a/lint-workflow/src/rules/job_environment_prefix.py b/lint-workflow/src/rules/job_environment_prefix.py index 18c2268f..6266a26d 100644 --- a/lint-workflow/src/rules/job_environment_prefix.py +++ b/lint-workflow/src/rules/job_environment_prefix.py @@ -1,23 +1,27 @@ +from typing import Tuple + from ..rule import Rule from ..models.job import Job +from ..utils import Settings class RuleJobEnvironmentPrefix(Rule): - def __init__(self): - self.message = f"Job Environment vars should start with and underscore:\n" + def __init__(self, settings: Settings = None) -> None: + self.message = f"Job Environment vars should start with and underscore:" self.on_fail = "error" - self.compability = [Job] + self.compatibility = [Job] + self.settings = settings - def fn(self, obj: Job): + def fn(self, obj: Job) -> Tuple[bool, str]: correct = True - message = "" + offending_keys = [] for key, value in obj.env.items(): if key[0] != "_": - message += f" {key}" + offending_keys.append(key) correct = False if correct: return True, "" - return False, f"{self.message}{message}" + return False, f"{self.message} ({' ,'.join(offending_keys)})" diff --git a/lint-workflow/src/rules/name_capitalized.py b/lint-workflow/src/rules/name_capitalized.py index 609aff5f..4926954e 100644 --- a/lint-workflow/src/rules/name_capitalized.py +++ b/lint-workflow/src/rules/name_capitalized.py @@ -1,17 +1,19 @@ -from typing import Union +from typing import Union, Tuple from ..rule import Rule from ..models.workflow import Workflow from ..models.job import Job from ..models.step import Step +from ..utils import Settings class RuleNameCapitalized(Rule): - def __init__(self): + def __init__(self, settings: Settings = None) -> None: self.message = "name must capitalized" self.on_fail = "error" + self.settings = settings - def fn(self, obj: Union[Workflow, Job, Step]): + def fn(self, obj: Union[Workflow, Job, Step]) -> Tuple[bool, str]: if obj.name: return obj.name[0].isupper(), self.message return True, "" # Force passing if obj.name doesn't exist diff --git a/lint-workflow/src/rules/name_exists.py b/lint-workflow/src/rules/name_exists.py index d7b7398c..8fe3e60a 100644 --- a/lint-workflow/src/rules/name_exists.py +++ b/lint-workflow/src/rules/name_exists.py @@ -1,17 +1,19 @@ -from typing import Union +from typing import Union, Tuple from ..rule import Rule from ..models.workflow import Workflow from ..models.job import Job from ..models.step import Step +from ..utils import Settings class RuleNameExists(Rule): - def __init__(self): + def __init__(self, settings: Settings = None) -> None: self.message = "name must exist" self.on_fail = "error" + self.settings = settings - def fn(self, obj: Union[Workflow, Job, Step]): + def fn(self, obj: Union[Workflow, Job, Step]) -> Tuple[bool, str]: if obj.name is not None: return True, "" return False, self.message diff --git a/lint-workflow/src/rules/pinned_job_runner.py b/lint-workflow/src/rules/pinned_job_runner.py index 95919c02..d4ce92a9 100644 --- a/lint-workflow/src/rules/pinned_job_runner.py +++ b/lint-workflow/src/rules/pinned_job_runner.py @@ -1,14 +1,18 @@ +from typing import Tuple + from ..rule import Rule from ..models.job import Job +from ..utils import Settings class RuleJobRunnerVersionPinned(Rule): - def __init__(self): + def __init__(self, settings: Settings = None) -> None: self.message = "Workflow runner must be pinned" self.on_fail = "error" self.compatibility = [Job] + self.settings = settings - def fn(self, obj: Job): + def fn(self, obj: Job) -> Tuple[bool, str]: if "latest" not in obj.runs_on: return True, "" return False, self.message diff --git a/lint-workflow/src/rules/step_approved.py b/lint-workflow/src/rules/step_approved.py index 9df84048..9cfe4ad9 100644 --- a/lint-workflow/src/rules/step_approved.py +++ b/lint-workflow/src/rules/step_approved.py @@ -43,14 +43,13 @@ def fn(self, obj: Step) -> Tuple[bool, str]: ), ) - action_data = self.settings.approved_actions_data[path] + action = self.settings.approved_actions[path] - if obj.uses_version != action_data["version"] or obj.uses_ref != action_data["sha"]: + if obj.uses_version != action.version or obj.uses_ref != action.sha: return False, ( "Action is out of date. Please update to:\n" - f" commit: {action_data['version']}" - f" version: {action_data['sha']}" + f" commit: {action.version}" + f" version: {action.sha}" ) - return True, "" diff --git a/lint-workflow/src/rules/step_pinned.py b/lint-workflow/src/rules/step_pinned.py index cc80b28d..2899b9ee 100644 --- a/lint-workflow/src/rules/step_pinned.py +++ b/lint-workflow/src/rules/step_pinned.py @@ -2,13 +2,15 @@ from ..rule import Rule from ..models.step import Step +from ..utils import Settings class RuleStepUsesPinned(Rule): - def __init__(self) -> None: + def __init__(self, settings: Settings = None) -> None: self.message = f"error" self.on_fail = "error" self.compatibility = [Step] + self.settings = settings def force_pass(self, obj: Step) -> bool: if not obj.uses: diff --git a/lint-workflow/src/utils.py b/lint-workflow/src/utils.py index 8f795888..74f8d34e 100644 --- a/lint-workflow/src/utils.py +++ b/lint-workflow/src/utils.py @@ -1,4 +1,19 @@ from dataclasses import dataclass +from typing import Self + + +@dataclass +class Colors: + """Class containing color codes for printing strings to output.""" + + black = "30m" + red = "31m" + green = "32m" + yellow = "33m" + blue = "34m" + magenta = "35m" + cyan = "36m" + white = "37m" @dataclass @@ -9,23 +24,38 @@ class LintFinding: level: str = None +@dataclass +class Action: + name: str + version: str = "" + sha: str = "" + + def __eq__(self, other: Self) -> bool: + return ( + self.name == other.name + and self.version == other.version + and self.sha == other.sha + ) + + def __ne__(self, other: Self) -> bool: + return not self.__eq__(other) + + +class SettingsError(Exception): + pass + + @dataclass class Settings: enabled_rules: list[str] - approved_actions_keys: set[str] - approved_actions_data: dict[str, str] + approved_actions: dict[str, Action] def __init__( self, enabled_rules: list[str] = None, - approved_actions: list[dict[str, str]]= None + approved_actions: dict[str, dict[str, str]] = None, ): self.enabled_rules = enabled_rules - self.approved_actions = set([action['name'] for action in approved_actions]) - self.approved_actions_data = { - action['name']: action for action in approved_actions + self.approved_actions = { + name: Action(**action) for name, action in approved_actions.items() } - - -class SettingsError(Exception): - pass diff --git a/lint-workflow/tests/rules/test_job_environment_prefix.py b/lint-workflow/tests/rules/test_job_environment_prefix.py index c77d3ef4..87528ffa 100644 --- a/lint-workflow/tests/rules/test_job_environment_prefix.py +++ b/lint-workflow/tests/rules/test_job_environment_prefix.py @@ -53,12 +53,30 @@ def rule(): def test_rule_on_correct_workflow(rule, correct_workflow): + obj = correct_workflow.jobs["job-key"] + result, message = rule.fn(correct_workflow.jobs["job-key"]) assert result == True assert message == "" + finding = rule.execute(obj) + assert finding is None + def test_rule_on_incorrect_workflow(rule, incorrect_workflow): - result, message = rule.fn(incorrect_workflow.jobs["job-key"]) + obj = incorrect_workflow.jobs["job-key"] + + result, message = rule.fn(obj) assert result == False assert "TEST_ENV" in message + + finding = rule.execute(obj) + assert "TEST_ENV" in finding.description + + +def test_fail_compatibility(rule, correct_workflow): + finding = rule.execute(correct_workflow) + assert "Workflow not compatible with" in finding.description + + finding = rule.execute(correct_workflow.jobs["job-key"].steps[0]) + assert "Step not compatible with" in finding.description diff --git a/lint-workflow/tests/rules/test_step_approved.py b/lint-workflow/tests/rules/test_step_approved.py index f0ea7e0a..2d3109b9 100644 --- a/lint-workflow/tests/rules/test_step_approved.py +++ b/lint-workflow/tests/rules/test_step_approved.py @@ -15,18 +15,20 @@ @pytest.fixture def settings(): - return Settings(approved_actions=[ - { - "name": "actions/checkout", - "version": "v4.1.1", - "sha": "b4ffde65f46336ab88eb53be808477a3936bae11" - }, - { - "name": "actions/download-artifact", - "version": "v4.1.0", - "sha": "f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110" + return Settings( + approved_actions={ + "actions/checkout": { + "name": "actions/checkout", + "version": "v4.1.1", + "sha": "b4ffde65f46336ab88eb53be808477a3936bae11", + }, + "actions/download-artifact": { + "name": "actions/download-artifact", + "version": "v4.1.0", + "sha": "f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110", + }, } - ]) + ) @pytest.fixture diff --git a/lint-workflow/tests/rules/test_step_pinned.py b/lint-workflow/tests/rules/test_step_pinned.py index 1a95f025..9206c74a 100644 --- a/lint-workflow/tests/rules/test_step_pinned.py +++ b/lint-workflow/tests/rules/test_step_pinned.py @@ -54,7 +54,6 @@ def incorrect_workflow(): return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) - @pytest.fixture def rule(): return RuleStepUsesPinned() From 81cc44956815b42ac914585dc3285bb3bab3bd86 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 5 Jan 2024 14:56:09 -0800 Subject: [PATCH 11/57] Add test coverage and get test coverage of the rules to 100% --- lint-workflow/Taskfile.yml | 12 +++- lint-workflow/cli.py | 58 +++++----------- lint-workflow/src/actions.py | 69 +++++++++++++++---- lint-workflow/src/lint.py | 25 ++++++- .../tests/rules/test_name_capitalized.py | 45 ++++++------ .../tests/rules/test_step_approved.py | 20 ++++++ lint-workflow/tests/rules/test_step_pinned.py | 20 ++++++ lint-workflow/tests/test_load.py | 3 +- lint-workflow/tests/test_rule.py | 19 +++++ lint-workflow/tests/test_utils.py | 32 +++++++++ 10 files changed, 222 insertions(+), 81 deletions(-) diff --git a/lint-workflow/Taskfile.yml b/lint-workflow/Taskfile.yml index 83315751..83c9b785 100644 --- a/lint-workflow/Taskfile.yml +++ b/lint-workflow/Taskfile.yml @@ -12,9 +12,17 @@ tasks: cmds: - pipenv run pytest tests - test:unit:cov: + test:unit: + cmds: + - pipenv run pytest tests + + test:cov: + cmds: + - pipenv run pytest --cov-report term --cov=src tests + + test:cov:detailed: cmds: - - pipenv run pytest tests --cov=src + - pipenv run pytest --cov-report term-missing --cov=src tests test:e2e:lint: cmds: diff --git a/lint-workflow/cli.py b/lint-workflow/cli.py index b5d995c5..f659e09c 100644 --- a/lint-workflow/cli.py +++ b/lint-workflow/cli.py @@ -6,13 +6,13 @@ # from src.rules import workflow_rules, job_rules, step_rules, uses_step_rules, run_step_rules import settings -from src.actions import Actions +from src.actions import ActionsCmd from src.utils import Settings, SettingsError -from src.lint import Linter +from src.lint import LinterCmd try: - lint_settings = Settings( + local_settings = Settings( enabled_rules=settings.enabled_rules, approved_actions=settings.approved_actions ) except: @@ -23,49 +23,24 @@ ) ) -linter = Linter(settings=lint_settings) -actions = Actions(settings=lint_settings) -# print(lint_rules.workflow) +def main(input_args=None): + """CLI utility to lint GitHub Action Workflows. + A CLI utility to enforce coding standards on GitHub Action workflows. The + utility also provides other sub-commands to assist with other workflow + maintenance tasks; such as maintaining the list of approved GitHub Actions. + """ + linter_cmd = LinterCmd(settings=local_settings) + actions_cmd = ActionsCmd(settings=local_settings) -def main(input_args=None): # Read arguments from command line. parser = argparse.ArgumentParser(prog="workflow-linter") parser.add_argument("-v", "--verbose", action="store_true", default=False) subparsers = parser.add_subparsers(required=True, dest="command") - parser_actions = subparsers.add_parser("actions", help="actions help") - parser_actions.add_argument( - "-o", "--output", action="store", default="actions.json" - ) - subparsers_actions = parser_actions.add_subparsers( - required=True, dest="actions_command" - ) - - parser_actions_update = subparsers_actions.add_parser( - "update", help="update action versions" - ) - - parser_actions_add = subparsers_actions.add_parser( - "add", help="add action to approved list" - ) - parser_actions_add.add_argument("name", help="action name [git owener/repo]") - - parser_lint = subparsers.add_parser("lint", help="lint help") - parser_lint.add_argument( - "-s", - "--strict", - action="store_true", - help="return non-zero exit code on warnings as well as errors", - ) - parser_lint.add_argument("-f", "--files", action="append", help="files to lint") - parser_lint.add_argument( - "--output", - action="store", - help="output format: [stdout|json|md]", - default="stdout", - ) + subparsers = LinterCmd.extend_parser(subparsers) + subparsers = ActionsCmd.extend_parser(subparsers) # Pull the arguments from the command line input_args = sys.argv[1:] @@ -78,17 +53,16 @@ def main(input_args=None): print(f"Args:\n{args}") if args.command == "lint": - return linter.run(args.files) + return linter_cmd.run(args.files) if args.command == "actions": if args.actions_command == "add": - return actions.add(args.name, args.output) + return actions_cmd.add(args.name, args.output) elif args.actions_command == "update": - return actions.update(args.output) + return actions_cmd.update(args.output) return -1 if __name__ == "__main__": return_code = main() - # print(memoized_action_update_urls) sys.exit(return_code) diff --git a/lint-workflow/src/actions.py b/lint-workflow/src/actions.py index e85f6c83..f25f9917 100644 --- a/lint-workflow/src/actions.py +++ b/lint-workflow/src/actions.py @@ -1,3 +1,4 @@ +import argparse import json import logging import os @@ -9,10 +10,34 @@ from src.utils import Colors, LintFinding, Settings, SettingsError, Action -class Actions: +class ActionsCmd: def __init__(self, settings: Settings = None) -> None: self.settings = settings + @staticmethod + def extend_parser(subparsers: argparse.ArgumentParser) -> argparse.ArgumentParser: + """Extends the CLI subparser with the options for ActionCmd. + + Add 'actions add' and 'actions update' to the CLI as sub-commands + along with the options and arguments for each. + """ + parser_actions = subparsers.add_parser("actions", help="actions help") + parser_actions.add_argument( + "-o", "--output", action="store", default="actions.json" + ) + subparsers_actions = parser_actions.add_subparsers( + required=True, dest="actions_command" + ) + parser_actions_update = subparsers_actions.add_parser( + "update", help="update action versions" + ) + parser_actions_add = subparsers_actions.add_parser( + "add", help="add action to approved list" + ) + parser_actions_add.add_argument("name", help="action name [git owener/repo]") + + return subparsers + def get_github_api_response( self, url: str, action_name: str ) -> Union[urllib.response.BaseHTTPResponse, None]: @@ -89,7 +114,26 @@ def get_latest_version(self, action: Action) -> Tuple[str, str]: return Action(name=action.name, version=tag_name, sha=sha) + def save_actions(self, updated_actions: dict[str, Action], filename: str) -> None: + """Save Actions to disk. + + This is used to track the list of approved actions. + """ + with open(filename, "w") as action_file: + converted_updated_actions = { + name: asdict(action) for name, action in updated_actions.items() + } + action_file.write( + json.dumps(converted_updated_actions, indent=2, sort_keys=True) + ) + def add(self, new_action_name: str, filename: str) -> None: + """Sub-command to add a new Action to the list of approved Actions. + + 'actions add' will add an Action and all of its metadata and dump all + approved actions (including the new one) to either the default JSON file + or the one provided by '--output' + """ print("Actions: add") updated_actions = self.settings.approved_actions proposed_action = Action(name=new_action_name) @@ -98,15 +142,15 @@ def add(self, new_action_name: str, filename: str) -> None: latest = self.get_latest_version(proposed_action) updated_actions[latest.name] = latest - with open(filename, "w") as action_file: - converted_updated_actions = { - name: asdict(action) for name, action in updated_actions.items() - } - action_file.write( - json.dumps(converted_updated_actions, indent=2, sort_keys=True) - ) + self.save_actions(updated_actions, filename) def update(self, filename: str) -> None: + """Sub-command to update all of the versions of the approved actions. + + 'actions update' will update all of the approved to the newest version + and dump all of the new data to either the default JSON file or the + one provided by '--output' + """ print("Actions: update") updated_actions = {} for action in self.settings.approved_actions.values(): @@ -123,10 +167,5 @@ def update(self, filename: str) -> None: print(f" - {action.name} \033[{Colors.green}ok\033[0m") updated_actions[action.name] = latest_release - with open(filename, "w") as action_file: - converted_updated_actions = { - name: asdict(action) for name, action in updated_actions.items() - } - action_file.write( - json.dumps(converted_updated_actions, indent=2, sort_keys=True) - ) + self.save_actions(updated_actions, filename) + diff --git a/lint-workflow/src/lint.py b/lint-workflow/src/lint.py index f08a85e6..97ba8c6f 100644 --- a/lint-workflow/src/lint.py +++ b/lint-workflow/src/lint.py @@ -1,3 +1,4 @@ +import argparse import os from src.load import WorkflowBuilder, Rules @@ -10,10 +11,32 @@ } -class Linter: +class LinterCmd: def __init__(self, settings: Settings = None, verbose: bool = True) -> None: self.rules = Rules(settings=settings, verbose=verbose) + @staticmethod + def extend_parser(subparsers: argparse.ArgumentParser) -> argparse.ArgumentParser: + """Extends the CLI subparser with the options for LintCmd. + + Add 'lint' as a sub command along with its options and arguments + """ + parser_lint = subparsers.add_parser("lint", help="lint help") + parser_lint.add_argument( + "-s", + "--strict", + action="store_true", + help="return non-zero exit code on warnings as well as errors", + ) + parser_lint.add_argument("-f", "--files", action="append", help="files to lint") + parser_lint.add_argument( + "--output", + action="store", + help="output format: [stdout|json|md]", + default="stdout", + ) + return subparsers + def get_max_error_level(self, findings: list[LintFinding]) -> int: """Get max error level from list of findings.""" if len(findings) == 0: diff --git a/lint-workflow/tests/rules/test_name_capitalized.py b/lint-workflow/tests/rules/test_name_capitalized.py index bee36fae..12acf926 100644 --- a/lint-workflow/tests/rules/test_name_capitalized.py +++ b/lint-workflow/tests/rules/test_name_capitalized.py @@ -13,14 +13,10 @@ @pytest.fixture def correct_workflow(): - return WorkflowBuilder.build(f"{FIXTURE_DIR}/test-min.yaml") - - -@pytest.fixture -def incorrect_workflow_name(): workflow = """\ --- -name: test +name: Test Workflow + on: workflow_dispatch: @@ -36,10 +32,10 @@ def incorrect_workflow_name(): @pytest.fixture -def incorrect_job_name(): +def incorrect_workflow(): workflow = """\ --- -name: Test +name: test on: workflow_dispatch: @@ -48,31 +44,29 @@ def incorrect_job_name(): name: test runs-on: ubuntu-latest steps: - - name: Test + - name: test run: echo test """ return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) @pytest.fixture -def incorrect_step_name(): +def missing_name_workflow(): workflow = """\ --- -name: Test on: workflow_dispatch: jobs: job-key: - name: Test runs-on: ubuntu-latest steps: - - name: test - run: echo test + - run: echo test """ return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) + @pytest.fixture def rule(): return RuleNameCapitalized() @@ -89,16 +83,27 @@ def test_rule_on_correct_workflow(rule, correct_workflow): assert result == True -def test_rule_on_incorrect_workflow_name(rule, incorrect_workflow_name): - result, message = rule.fn(incorrect_workflow_name) +def test_rule_on_incorrect_workflow_name(rule, incorrect_workflow): + result, message = rule.fn(incorrect_workflow) assert result == False -def test_rule_on_incorrect_workflow_name(rule, incorrect_job_name): - result, message = rule.fn(incorrect_job_name.jobs["job-key"]) +def test_rule_on_incorrect_job_name(rule, incorrect_workflow): + result, message = rule.fn(incorrect_workflow.jobs["job-key"]) assert result == False -def test_rule_on_incorrect_workflow_name(rule, incorrect_step_name): - result, message = rule.fn(incorrect_step_name.jobs["job-key"].steps[0]) +def test_rule_on_incorrect_step_name(rule, incorrect_workflow): + result, message = rule.fn(incorrect_workflow.jobs["job-key"].steps[0]) assert result == False + + +def test_rule_on_missing_names(rule, missing_name_workflow): + result, message = rule.fn(missing_name_workflow) + assert result == True + + result, message = rule.fn(missing_name_workflow.jobs["job-key"]) + assert result == True + + result, message = rule.fn(missing_name_workflow.jobs["job-key"].steps[0]) + assert result == True diff --git a/lint-workflow/tests/rules/test_step_approved.py b/lint-workflow/tests/rules/test_step_approved.py index 2d3109b9..7f2d83a7 100644 --- a/lint-workflow/tests/rules/test_step_approved.py +++ b/lint-workflow/tests/rules/test_step_approved.py @@ -47,6 +47,12 @@ def correct_workflow(): - name: Test Bitwarden Action uses: bitwarden/gh-actions/get-keyvault-secrets@main + + - name: Test Local Action + uses: ./actions/test-action + + - name: Test Run Action + run: echo "test" """ return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) @@ -83,6 +89,12 @@ def test_rule_on_correct_workflow(rule, correct_workflow): result, message = rule.fn(correct_workflow.jobs["job-key"].steps[1]) assert result == True + result, message = rule.fn(correct_workflow.jobs["job-key"].steps[2]) + assert result == True + + result, message = rule.fn(correct_workflow.jobs["job-key"].steps[3]) + assert result == True + def test_rule_on_incorrect_workflow(rule, incorrect_workflow): result, message = rule.fn(incorrect_workflow.jobs["job-key"].steps[0]) @@ -92,3 +104,11 @@ def test_rule_on_incorrect_workflow(rule, incorrect_workflow): result, message = rule.fn(incorrect_workflow.jobs["job-key"].steps[1]) assert result == False assert "Action is out of date" in message + + +def test_fail_compatibility(rule, correct_workflow): + finding = rule.execute(correct_workflow) + assert "Workflow not compatible with" in finding.description + + finding = rule.execute(correct_workflow.jobs["job-key"]) + assert "Job not compatible with" in finding.description diff --git a/lint-workflow/tests/rules/test_step_pinned.py b/lint-workflow/tests/rules/test_step_pinned.py index 9206c74a..c917c769 100644 --- a/lint-workflow/tests/rules/test_step_pinned.py +++ b/lint-workflow/tests/rules/test_step_pinned.py @@ -27,6 +27,12 @@ def correct_workflow(): - name: Test Internal Action uses: bitwarden/gh-actions/get-keyvault-secrets@main + + - name: Test Local Action + uses: ./actions/test-action + + - name: Test Run Action + run: echo "test" """ return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) @@ -66,6 +72,12 @@ def test_rule_on_correct_workflow(rule, correct_workflow): result, message = rule.fn(correct_workflow.jobs["job-key"].steps[1]) assert result == True + result, message = rule.fn(correct_workflow.jobs["job-key"].steps[2]) + assert result == True + + result, message = rule.fn(correct_workflow.jobs["job-key"].steps[3]) + assert result == True + def test_rule_on_incorrect_workflow_external_branch(rule, incorrect_workflow): result, message = rule.fn(incorrect_workflow.jobs["job-key"].steps[0]) @@ -83,3 +95,11 @@ def test_rule_on_incorrect_workflow_internal_commit(rule, incorrect_workflow): result, message = rule.fn(incorrect_workflow.jobs["job-key"].steps[2]) assert result == False assert "Please pin to main" in message + + +def test_fail_compatibility(rule, correct_workflow): + finding = rule.execute(correct_workflow) + assert "Workflow not compatible with" in finding.description + + finding = rule.execute(correct_workflow.jobs["job-key"]) + assert "Job not compatible with" in finding.description diff --git a/lint-workflow/tests/test_load.py b/lint-workflow/tests/test_load.py index 7ed6317e..38131563 100644 --- a/lint-workflow/tests/test_load.py +++ b/lint-workflow/tests/test_load.py @@ -7,6 +7,8 @@ from .conftest import FIXTURE_DIR from .context import src +from src.utils import Settings + yaml = YAML() @@ -43,5 +45,4 @@ def test_load_workflow_from_file(workflow_filename: str) -> None: def test_load_workflow_from_yaml(workflow_yaml: CommentedMap) -> None: workflow = src.load.WorkflowBuilder.build(yaml=workflow_yaml, from_file=False) - assert type(workflow) == src.models.Workflow diff --git a/lint-workflow/tests/test_rule.py b/lint-workflow/tests/test_rule.py index 5fb5e782..4e43a8a1 100644 --- a/lint-workflow/tests/test_rule.py +++ b/lint-workflow/tests/test_rule.py @@ -67,6 +67,16 @@ def fn(self, obj: Union[Workflow, Job, Step]) -> bool: return obj.name is not None, self.message +class RuleException(Rule): + def __init__(self): + self.message = "should raise Exception" + self.on_fail = "error" + + def fn(self, obj: Union[Workflow, Job, Step]) -> bool: + raise Exception("test Exception") + return True, self.message + + @pytest.fixture def step_rule(): return RuleStep() @@ -77,6 +87,11 @@ def exists_rule(): return RuleNameExists() +@pytest.fixture +def exception_rule(): + return RuleException() + + def test_build_lint_message(step_rule, correct_workflow): assert step_rule.build_lint_message("test", correct_workflow) == "Workflow => test" @@ -119,3 +134,7 @@ def test_incorrect_rule_execution(exists_rule, incorrect_workflow): "name must exist" in exists_rule.execute(incorrect_workflow.jobs["job-key"].steps[0]).description ) + + +def test_exception_rule_execution(exception_rule, incorrect_workflow): + assert "failed to apply" in exception_rule.execute(incorrect_workflow).description diff --git a/lint-workflow/tests/test_utils.py b/lint-workflow/tests/test_utils.py index 87c6d5fa..e9b667c8 100644 --- a/lint-workflow/tests/test_utils.py +++ b/lint-workflow/tests/test_utils.py @@ -6,3 +6,35 @@ from .conftest import FIXTURE_DIR from .context import src + +from src.utils import Action + + +def test_action_eq(): + action_def = { + "name": "bitwarden/sm-action", + "version": "1.0.0", + "sha": "some-sha" + } + + action_a = Action(**action_def) + action_b = Action(**action_def) + + assert (action_a == action_b) == True + assert (action_a != action_b) == False + + +def test_action_ne(): + action_a = Action( + name = "bitwarden/sm-action", + version = "1.0.0", + sha = "some-sha" + ) + action_b = Action( + name = "bitwarden/sm-action", + version = "1.1.0", + sha = "some-other-sha" + ) + + assert (action_a == action_b) == False + assert (action_a != action_b) == True From 8e4f866d2406b457595c01965793975a74058d9c Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 5 Jan 2024 15:01:59 -0800 Subject: [PATCH 12/57] Update Python version in lint-ci to match refactored linter --- .github/workflows/lint-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint-ci.yml b/.github/workflows/lint-ci.yml index e61701fc..d82c394c 100644 --- a/.github/workflows/lint-ci.yml +++ b/.github/workflows/lint-ci.yml @@ -19,7 +19,7 @@ jobs: - name: Set up Python uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 with: - python-version: "3.9" + python-version: "3.11" - name: Install dependencies working-directory: lint-workflow @@ -30,4 +30,4 @@ jobs: - name: Test lint working-directory: lint-workflow - run: pipenv run pytest tests + run: pipenv run pytest tests --cov=src From e0a3ad8875dc1d44ee5a7557673a3b91f58feeb2 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 5 Jan 2024 15:14:24 -0800 Subject: [PATCH 13/57] Removing old files. Update verbosity --- lint-workflow/cli.py | 3 - lint-workflow/src/lint.py | 6 +- lint-workflow/src/load.py | 26 +++---- lint-workflow/src/rules.py | 86 ---------------------- lint-workflow/tests/test_action_update.py | 20 ----- lint-workflow/tests/test_lint.py | 23 ------ lint-workflow/tests/test_main.py | 66 ----------------- lint-workflow/tests/test_workflow_files.py | 37 ---------- 8 files changed, 16 insertions(+), 251 deletions(-) delete mode 100644 lint-workflow/src/rules.py delete mode 100644 lint-workflow/tests/test_action_update.py delete mode 100644 lint-workflow/tests/test_lint.py delete mode 100644 lint-workflow/tests/test_main.py delete mode 100644 lint-workflow/tests/test_workflow_files.py diff --git a/lint-workflow/cli.py b/lint-workflow/cli.py index f659e09c..7372f26a 100644 --- a/lint-workflow/cli.py +++ b/lint-workflow/cli.py @@ -49,9 +49,6 @@ def main(input_args=None): args = parser.parse_args(input_args) - if args.verbose: - print(f"Args:\n{args}") - if args.command == "lint": return linter_cmd.run(args.files) diff --git a/lint-workflow/src/lint.py b/lint-workflow/src/lint.py index 97ba8c6f..0bc37607 100644 --- a/lint-workflow/src/lint.py +++ b/lint-workflow/src/lint.py @@ -12,8 +12,8 @@ class LinterCmd: - def __init__(self, settings: Settings = None, verbose: bool = True) -> None: - self.rules = Rules(settings=settings, verbose=verbose) + def __init__(self, settings: Settings = None) -> None: + self.rules = Rules(settings=settings) @staticmethod def extend_parser(subparsers: argparse.ArgumentParser) -> argparse.ArgumentParser: @@ -62,7 +62,7 @@ def lint_file(self, filename: str) -> int: findings = [] max_error_level = 0 - print(f"Linting: {filename}\n") + print(f"Linting: {filename}") with open(filename) as file: workflow = WorkflowBuilder.build(filename) diff --git a/lint-workflow/src/load.py b/lint-workflow/src/load.py index 7f9eed0b..61c70f4c 100644 --- a/lint-workflow/src/load.py +++ b/lint-workflow/src/load.py @@ -64,7 +64,7 @@ class Rules: job: List[Rule] = [] step: List[Rule] = [] - def __init__(self, settings: Settings, verbose: bool = False) -> None: + def __init__(self, settings: Settings) -> None: for rule in settings.enabled_rules: module_name = rule.split(".") module_name = ".".join(module_name[:-1]) @@ -83,15 +83,15 @@ def __init__(self, settings: Settings, verbose: bool = False) -> None: except Exception as err: print(f"Error loading: {rule}\n{err}") - if verbose: - print("===== Loaded Rules =====") - print("workflow rules:") - for rule in self.workflow: - print(f" - {type(rule).__name__}") - print("job rules:") - for rule in self.job: - print(f" - {type(rule).__name__}") - print("step rules:") - for rule in self.step: - print(f" - {type(rule).__name__}") - print("========================\n") + def list(self) -> None: + print("===== Loaded Rules =====") + print("workflow rules:") + for rule in self.workflow: + print(f" - {type(rule).__name__}") + print("job rules:") + for rule in self.job: + print(f" - {type(rule).__name__}") + print("step rules:") + for rule in self.step: + print(f" - {type(rule).__name__}") + print("========================\n") diff --git a/lint-workflow/src/rules.py b/lint-workflow/src/rules.py deleted file mode 100644 index 18922588..00000000 --- a/lint-workflow/src/rules.py +++ /dev/null @@ -1,86 +0,0 @@ -from collections.abc import Callable -from dataclasses import dataclass -from typing import Union - -from .models.workflow import Workflow -from .models.job import Job -from .models.step import Step - - -@dataclass -class LintFinding: - """Represents a linting problem.""" - description: str = "" - level: str = None - - -def validate( - obj: Union[Workflow, Job, Step], - rule: Callable[Union[Workflow, Job, Step], Union[bool, None]], - message: str, - warning_level: str -) -> Union[LintFinding, None]: - try: - if rule(obj): - return None - except: - message = f"failed to apply {rule.__name__}" - warning_level = "error" - - return LintFinding(f"{obj.__name__}.{obj.name} => {message}", warning_level) - - -class Rule: - def __init__( - self, - fn: Callable[Union[Workflow, Job, Step], Union[bool, None]], - message: str = "error", - warning_level: str = "error", - ): - self.fn = fn - self.message = message - self.warning_level = warning_level - - def execute(self, obj: Union[Workflow, Job, Step]): - try: - if self.fn(obj): - return None - except: - message = f"failed to apply {self.fn.__name__}" - warning_level = "error" - - #return LintFinding(f"{obj.__name__}.{obj.name} => {self.message}", self.warning_level) - return LintFinding(f"{obj.name} => {self.message}", self.warning_level) - - -# -------- Rules --------- - -def name_exists(obj: Union[Workflow, Job, Step]): - return obj.name is not None - -def name_capitalized(obj: Union[Workflow, Job, Step]): - return obj.name.isupper() - - -def step_run_single_line(step: Step): - return True - -# ----- End of Rules ----- - -workflow_rules = [ - Rule(name_exists, "field required", "error"), - Rule(name_capitalized, "field must be capitalized", "error") -] -job_rules = [ - Rule(name_exists, "field required", "error"), - Rule(name_capitalized, "field must be capitalized", "error") -] -step_rules = [ - Rule(name_exists, "field required", "error"), - Rule(name_capitalized, "field must be capitalized", "error") -] -uses_step_rules = [ -] -run_step_rules = [ -] - diff --git a/lint-workflow/tests/test_action_update.py b/lint-workflow/tests/test_action_update.py deleted file mode 100644 index b2c12e6f..00000000 --- a/lint-workflow/tests/test_action_update.py +++ /dev/null @@ -1,20 +0,0 @@ -import pytest -import urllib3 as urllib - -from lint import get_action_update, memoized_action_update_urls - -http = urllib.PoolManager() - - -@pytest.mark.skip() -def test_action_update(): - action_id = "actions/checkout@86f86b36ef15e6570752e7175f451a512eac206b" - sub_string = "github.com" - update_url = get_action_update(action_id) - assert str(sub_string) in str(update_url) - - r = http.request("GET", update_url) - - assert r.status == 200 - - assert "actions/checkout" in memoized_action_update_urls diff --git a/lint-workflow/tests/test_lint.py b/lint-workflow/tests/test_lint.py deleted file mode 100644 index 4f89a084..00000000 --- a/lint-workflow/tests/test_lint.py +++ /dev/null @@ -1,23 +0,0 @@ -import pytest - -from lint import lint -from .configs import FIXTURES_DIR - - -@pytest.mark.skip() -def test_lint(capfd): - file_path = f"{FIXTURES_DIR}/test.yml" - lint_output = lint(file_path) - out, err = capfd.readouterr() - assert ( - "\x1b[33mwarning\x1b[0m Name value for workflow is not capitalized. [crowdin Pull]" - in out - ) - assert ( - "\x1b[33mwarning\x1b[0m Step 4 of job key 'crowdin-pull' uses an outdated action, consider updating it" - in out - ) - assert ( - "\x1b[31merror\x1b[0m Step 2 of job key 'crowdin-pull' uses an non-existing action: Azure/logi@77f1b2e3fb80c0e8645114159d17008b8a2e475a." - in out - ) diff --git a/lint-workflow/tests/test_main.py b/lint-workflow/tests/test_main.py deleted file mode 100644 index bc4f863a..00000000 --- a/lint-workflow/tests/test_main.py +++ /dev/null @@ -1,66 +0,0 @@ -import pytest - -from lint import main -from .configs import FIXTURES_DIR - -# Tests for argparse inputs and outputs using capsys.readouterr() - -FIXTURES_DIR = "./tests/fixtures" - - -@pytest.mark.skip() -def test_main_single_file(capsys): - main([f"{FIXTURES_DIR}/test.yml"]) - captured = capsys.readouterr() - result = captured.out - assert "test.yml" in result - - -@pytest.mark.skip() -def test_main_multiple_files(capsys): - main([f"{FIXTURES_DIR}/test.yml {FIXTURES_DIR}/test-alt.yml"]) - captured = capsys.readouterr() - result = captured.out - assert isinstance(result, str) - assert "test.yml" in result - assert "test-alt.yml" in result - - -@pytest.mark.skip() -def test_main_folder(capsys): - main([f"{FIXTURES_DIR}"]) - captured = capsys.readouterr() - result = captured.out - assert isinstance(result, str) - assert "test.yml" in result - assert "test-alt.yml" in result - - -@pytest.mark.skip() -def test_main_folder_and_files(capsys): - main([f"{FIXTURES_DIR}/test.yml {FIXTURES_DIR}"]) - captured = capsys.readouterr() - result = captured.out - print(result) - - -@pytest.mark.skip() -def test_main_not_found(capsys): - # File that doesn't exist - main(["not-a-real-file.yml"]) - captured = capsys.readouterr() - result = captured.out - assert isinstance(result, str) - assert 'File(s)/Directory: "not-a-real-file.yml" does not exist, exiting.' in result - # Empty string - main([""]) - captured = capsys.readouterr() - result = captured.out - assert isinstance(result, str) - assert 'File(s)/Directory: "" does not exist, exiting.' in result - # Spaces in string - main([" "]) - captured = capsys.readouterr() - result = captured.out - assert isinstance(result, str) - assert 'File(s)/Directory: " " does not exist, exiting.' in result diff --git a/lint-workflow/tests/test_workflow_files.py b/lint-workflow/tests/test_workflow_files.py deleted file mode 100644 index 6d969306..00000000 --- a/lint-workflow/tests/test_workflow_files.py +++ /dev/null @@ -1,37 +0,0 @@ -import os -import pytest - -from lint import workflow_files -from .configs import FIXTURES_DIR - - -@pytest.mark.skip() -def test_workflow_files(): - assert workflow_files("") == [] - assert workflow_files("not-a-real-file.yml") == [] - assert workflow_files(f"{FIXTURES_DIR}/test.yml") == [f"{FIXTURES_DIR}/test.yml"] - # multiple files - assert workflow_files( - f"{FIXTURES_DIR}/test.yml {FIXTURES_DIR}/test-alt.yml" - ) == sorted([f"{FIXTURES_DIR}/test.yml", f"{FIXTURES_DIR}/test-alt.yml"]) - # directory - assert workflow_files(FIXTURES_DIR) == sorted( - set( - [ - f"{FIXTURES_DIR}/{file}" - for file in os.listdir(FIXTURES_DIR) - if file.endswith((".yml", ".yaml")) - ] - ) - ) - # directory and files - assert workflow_files(f"{FIXTURES_DIR}/test.yml {FIXTURES_DIR}") == sorted( - set( - [f"{FIXTURES_DIR}/test.yml"] - + [ - f"{FIXTURES_DIR}/{file}" - for file in os.listdir(FIXTURES_DIR) - if file.endswith((".yml", ".yaml")) - ] - ) - ) From 819ca78d30b092d9cd934ad28a4f4bbc7f1b19f7 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Mon, 8 Jan 2024 16:23:21 -0800 Subject: [PATCH 14/57] Finish in-code documentation --- lint-workflow/Pipfile | 1 + lint-workflow/Pipfile.lock | 50 +++- lint-workflow/Session.vim | 252 +++++++++++------- lint-workflow/cli.py | 2 +- lint-workflow/src/actions.py | 21 +- lint-workflow/src/lint.py | 107 +++++--- lint-workflow/src/load.py | 47 ++++ lint-workflow/src/rule.py | 46 +++- .../src/rules/job_environment_prefix.py | 35 ++- lint-workflow/src/rules/name_capitalized.py | 29 +- lint-workflow/src/rules/name_exists.py | 28 +- lint-workflow/src/rules/pinned_job_runner.py | 27 +- lint-workflow/src/rules/step_approved.py | 52 +++- lint-workflow/src/rules/step_pinned.py | 47 +++- lint-workflow/src/utils.py | 70 ++++- lint-workflow/tests/test_lint.py | 34 +++ lint-workflow/tests/test_utils.py | 16 +- 17 files changed, 685 insertions(+), 179 deletions(-) create mode 100644 lint-workflow/tests/test_lint.py diff --git a/lint-workflow/Pipfile b/lint-workflow/Pipfile index 6c0c5757..6918fd79 100644 --- a/lint-workflow/Pipfile +++ b/lint-workflow/Pipfile @@ -15,6 +15,7 @@ black = "*" pytest = "*" coverage = "*" pytest-cov = "*" +pylint = "*" [requires] python_version = "3.11" diff --git a/lint-workflow/Pipfile.lock b/lint-workflow/Pipfile.lock index 8635bd2a..5822b002 100644 --- a/lint-workflow/Pipfile.lock +++ b/lint-workflow/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "9c217f65befc18770734bee9ec6ee8420cdbad03a5eba07d13a77d3f0d082712" + "sha256": "c238af6c0c1de4a035edbf90e66bdbd46d0fd8ce05f368d1c89dd668427f6755" }, "pipfile-spec": 6, "requires": { @@ -320,6 +320,14 @@ } }, "develop": { + "astroid": { + "hashes": [ + "sha256:4a61cf0a59097c7bb52689b0fd63717cd2a8a14dc9f1eee97b82d814881c8c91", + "sha256:d6e62862355f60e716164082d6b4b041d38e2a8cf1c7cd953ded5108bac8ff5c" + ], + "markers": "python_full_version >= '3.8.0'", + "version": "==3.0.2" + }, "black": { "hashes": [ "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50", @@ -414,6 +422,14 @@ "index": "pypi", "version": "==7.4.0" }, + "dill": { + "hashes": [ + "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e", + "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03" + ], + "markers": "python_version >= '3.11'", + "version": "==0.3.7" + }, "iniconfig": { "hashes": [ "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", @@ -422,6 +438,22 @@ "markers": "python_version >= '3.7'", "version": "==2.0.0" }, + "isort": { + "hashes": [ + "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", + "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6" + ], + "markers": "python_full_version >= '3.8.0'", + "version": "==5.13.2" + }, + "mccabe": { + "hashes": [ + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.0" + }, "mypy-extensions": { "hashes": [ "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", @@ -462,6 +494,14 @@ "markers": "python_version >= '3.8'", "version": "==1.3.0" }, + "pylint": { + "hashes": [ + "sha256:58c2398b0301e049609a8429789ec6edf3aabe9b6c5fec916acd18639c16de8b", + "sha256:7a1585285aefc5165db81083c3e06363a27448f6b467b3b0f30dbd0ac1f73810" + ], + "index": "pypi", + "version": "==3.0.3" + }, "pytest": { "hashes": [ "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", @@ -477,6 +517,14 @@ ], "index": "pypi", "version": "==4.1.0" + }, + "tomlkit": { + "hashes": [ + "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4", + "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba" + ], + "markers": "python_version >= '3.7'", + "version": "==0.12.3" } } } diff --git a/lint-workflow/Session.vim b/lint-workflow/Session.vim index 2d3bdb42..5a3a81a0 100644 --- a/lint-workflow/Session.vim +++ b/lint-workflow/Session.vim @@ -14,43 +14,46 @@ else set shortmess=aoO endif badd +1 README.md -badd +1 cli.py +badd +24 cli.py badd +4 src/load.py badd +1 config.yaml badd +1 src/rules/__init__.py -badd +16 src/rules/name_capitalized.py -badd +17 src/rules/name_exists.py +badd +19 src/rules/name_capitalized.py +badd +13 src/rules/name_exists.py badd +1 src/rule.py badd +72 tests/test_rule.py badd +40 tests/rules/test_name_exists.py -badd +2 tests/rules/test_name_capitalized.py +badd +100 tests/rules/test_name_capitalized.py badd +4 rule_settings.py -badd +3 settings.py -badd +1 tests/fixtures/test.yml +badd +12 settings.py +badd +3 tests/fixtures/test.yml badd +1 tests/fixtures/test-min-incorrect.yaml badd +10 tests/test_load.py badd +2 src/rules/runs_on_pinned.py badd +3 src/rules/pinned_workflow_runner.py badd +3 tests/rules/test_pinned_workflow_runner.py badd +15 src/models/workflow.py -badd +9 src/rules/pinned_job_runner.py +badd +1 src/rules/pinned_job_runner.py badd +9 tests/rules/test_pinned_job_runner.py -badd +3 src/rules/job_environment_prefix.py -badd +14 src/models/job.py -badd +4 tests/rules/test_job_environment_prefix.py +badd +19 src/rules/job_environment_prefix.py +badd +18 src/models/job.py +badd +72 tests/rules/test_job_environment_prefix.py badd +19 src/rules/step_hex_length.py badd +43 tests/rules/test_step_hex_length.py badd +5 src/rules/step_hex.py badd +9 tests/rules/test_step_hex.py -badd +26 src/rules/step_approved.py -badd +59 tests/rules/test_step_approved.py +badd +47 src/rules/step_approved.py +badd +92 tests/rules/test_step_approved.py badd +0 src/utils.py -badd +28 src/rules/step_pinned.py -badd +82 tests/rules/test_step_pinned.py -badd +0 tests/fixtures/test-min.yaml +badd +16 src/rules/step_pinned.py +badd +100 tests/rules/test_step_pinned.py +badd +1 tests/fixtures/test-min.yaml badd +0 actions.json badd +0 src/models/step.py badd +0 tests/test_utils.py +badd +0 src/lint.py +badd +0 src/actions.py +badd +0 Taskfile.yml argglobal %argdel $argadd README.md @@ -58,6 +61,7 @@ set stal=2 tabnew +setlocal\ bufhidden=wipe tabnew +setlocal\ bufhidden=wipe tabnew +setlocal\ bufhidden=wipe +tabnew +setlocal\ bufhidden=wipe tabrewind edit README.md let s:save_splitbelow = &splitbelow @@ -70,6 +74,10 @@ vsplit 2wincmd h wincmd w wincmd w +wincmd _ | wincmd | +split +1wincmd k +wincmd w let &splitbelow = s:save_splitbelow let &splitright = s:save_splitright wincmd t @@ -79,11 +87,37 @@ set winminheight=0 set winheight=1 set winminwidth=0 set winwidth=1 -exe 'vert 1resize ' . ((&columns * 108 + 163) / 327) +exe 'vert 1resize ' . ((&columns * 109 + 163) / 327) exe 'vert 2resize ' . ((&columns * 108 + 163) / 327) -exe 'vert 3resize ' . ((&columns * 109 + 163) / 327) +exe '3resize ' . ((&lines * 32 + 33) / 67) +exe 'vert 3resize ' . ((&columns * 108 + 163) / 327) +exe '4resize ' . ((&lines * 31 + 33) / 67) +exe 'vert 4resize ' . ((&columns * 108 + 163) / 327) argglobal -balt tests/fixtures/test.yml +balt Taskfile.yml +setlocal fdm=manual +setlocal fde=0 +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=0 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +silent! normal! zE +let &fdl = &fdl +let s:l = 41 - ((36 * winheight(0) + 32) / 64) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 41 +normal! 0 +wincmd w +argglobal +if bufexists(fnamemodify("Taskfile.yml", ":p")) | buffer Taskfile.yml | else | edit Taskfile.yml | endif +if &buftype ==# 'terminal' + silent file Taskfile.yml +endif +balt README.md setlocal fdm=manual setlocal fde=0 setlocal fmr={{{,}}} @@ -94,12 +128,12 @@ setlocal fdn=20 setlocal fen silent! normal! zE let &fdl = &fdl -let s:l = 61 - ((57 * winheight(0) + 32) / 65) +let s:l = 29 - ((28 * winheight(0) + 32) / 64) if s:l < 1 | let s:l = 1 | endif keepjumps exe s:l normal! zt -keepjumps 61 -normal! 04| +keepjumps 29 +normal! 021| wincmd w argglobal if bufexists(fnamemodify("settings.py", ":p")) | buffer settings.py | else | edit settings.py | endif @@ -117,12 +151,12 @@ setlocal fdn=20 setlocal fen silent! normal! zE let &fdl = &fdl -let s:l = 6 - ((5 * winheight(0) + 32) / 65) +let s:l = 5 - ((4 * winheight(0) + 16) / 32) if s:l < 1 | let s:l = 1 | endif keepjumps exe s:l normal! zt -keepjumps 6 -normal! 019| +keepjumps 5 +normal! 0 wincmd w argglobal if bufexists(fnamemodify("actions.json", ":p")) | buffer actions.json | else | edit actions.json | endif @@ -140,16 +174,19 @@ setlocal fdn=20 setlocal fen silent! normal! zE let &fdl = &fdl -let s:l = 6 - ((5 * winheight(0) + 32) / 65) +let s:l = 5 - ((2 * winheight(0) + 15) / 31) if s:l < 1 | let s:l = 1 | endif keepjumps exe s:l normal! zt -keepjumps 6 -normal! 068| +keepjumps 5 +normal! 021| wincmd w -exe 'vert 1resize ' . ((&columns * 108 + 163) / 327) +exe 'vert 1resize ' . ((&columns * 109 + 163) / 327) exe 'vert 2resize ' . ((&columns * 108 + 163) / 327) -exe 'vert 3resize ' . ((&columns * 109 + 163) / 327) +exe '3resize ' . ((&lines * 32 + 33) / 67) +exe 'vert 3resize ' . ((&columns * 108 + 163) / 327) +exe '4resize ' . ((&lines * 31 + 33) / 67) +exe 'vert 4resize ' . ((&columns * 108 + 163) / 327) tabnext edit cli.py let s:save_splitbelow = &splitbelow @@ -175,7 +212,7 @@ exe 'vert 1resize ' . ((&columns * 108 + 163) / 327) exe 'vert 2resize ' . ((&columns * 108 + 163) / 327) exe 'vert 3resize ' . ((&columns * 109 + 163) / 327) argglobal -balt settings.py +balt src/actions.py setlocal fdm=manual setlocal fde=0 setlocal fmr={{{,}}} @@ -186,19 +223,19 @@ setlocal fdn=20 setlocal fen silent! normal! zE let &fdl = &fdl -let s:l = 21 - ((20 * winheight(0) + 32) / 65) +let s:l = 50 - ((49 * winheight(0) + 32) / 64) if s:l < 1 | let s:l = 1 | endif keepjumps exe s:l normal! zt -keepjumps 21 -normal! 0 +keepjumps 50 +normal! 09| wincmd w argglobal -if bufexists(fnamemodify("src/utils.py", ":p")) | buffer src/utils.py | else | edit src/utils.py | endif +if bufexists(fnamemodify("src/actions.py", ":p")) | buffer src/actions.py | else | edit src/actions.py | endif if &buftype ==# 'terminal' - silent file src/utils.py + silent file src/actions.py endif -balt tests/test_utils.py +balt cli.py setlocal fdm=manual setlocal fde=0 setlocal fmr={{{,}}} @@ -209,19 +246,19 @@ setlocal fdn=20 setlocal fen silent! normal! zE let &fdl = &fdl -let s:l = 25 - ((24 * winheight(0) + 32) / 65) +let s:l = 16 - ((10 * winheight(0) + 32) / 64) if s:l < 1 | let s:l = 1 | endif keepjumps exe s:l normal! zt -keepjumps 25 -normal! 035| +keepjumps 16 +normal! 0 wincmd w argglobal -if bufexists(fnamemodify("tests/test_utils.py", ":p")) | buffer tests/test_utils.py | else | edit tests/test_utils.py | endif +if bufexists(fnamemodify("src/lint.py", ":p")) | buffer src/lint.py | else | edit src/lint.py | endif if &buftype ==# 'terminal' - silent file tests/test_utils.py + silent file src/lint.py endif -balt tests/test_load.py +balt cli.py setlocal fdm=manual setlocal fde=0 setlocal fmr={{{,}}} @@ -232,18 +269,19 @@ setlocal fdn=20 setlocal fen silent! normal! zE let &fdl = &fdl -let s:l = 1 - ((0 * winheight(0) + 32) / 65) +let s:l = 65 - ((1 * winheight(0) + 32) / 64) if s:l < 1 | let s:l = 1 | endif keepjumps exe s:l normal! zt -keepjumps 1 -normal! 0 +keepjumps 65 +normal! 036| wincmd w +3wincmd w exe 'vert 1resize ' . ((&columns * 108 + 163) / 327) exe 'vert 2resize ' . ((&columns * 108 + 163) / 327) exe 'vert 3resize ' . ((&columns * 109 + 163) / 327) tabnext -edit src/models/job.py +edit src/load.py let s:save_splitbelow = &splitbelow let s:save_splitright = &splitright set splitbelow splitright @@ -251,7 +289,10 @@ wincmd _ | wincmd | vsplit wincmd _ | wincmd | vsplit -2wincmd h +wincmd _ | wincmd | +vsplit +3wincmd h +wincmd w wincmd w wincmd w let &splitbelow = s:save_splitbelow @@ -263,11 +304,12 @@ set winminheight=0 set winheight=1 set winminwidth=0 set winwidth=1 -exe 'vert 1resize ' . ((&columns * 108 + 163) / 327) -exe 'vert 2resize ' . ((&columns * 108 + 163) / 327) -exe 'vert 3resize ' . ((&columns * 109 + 163) / 327) +exe 'vert 1resize ' . ((&columns * 81 + 163) / 327) +exe 'vert 2resize ' . ((&columns * 81 + 163) / 327) +exe 'vert 3resize ' . ((&columns * 81 + 163) / 327) +exe 'vert 4resize ' . ((&columns * 81 + 163) / 327) argglobal -balt src/rule.py +balt tests/test_load.py setlocal fdm=manual setlocal fde=0 setlocal fmr={{{,}}} @@ -278,19 +320,19 @@ setlocal fdn=20 setlocal fen silent! normal! zE let &fdl = &fdl -let s:l = 18 - ((17 * winheight(0) + 32) / 65) +let s:l = 67 - ((32 * winheight(0) + 32) / 64) if s:l < 1 | let s:l = 1 | endif keepjumps exe s:l normal! zt -keepjumps 18 -normal! 016| +keepjumps 67 +normal! 042| wincmd w argglobal -if bufexists(fnamemodify("src/rule.py", ":p")) | buffer src/rule.py | else | edit src/rule.py | endif +if bufexists(fnamemodify("tests/test_load.py", ":p")) | buffer tests/test_load.py | else | edit tests/test_load.py | endif if &buftype ==# 'terminal' - silent file src/rule.py + silent file tests/test_load.py endif -balt tests/test_rule.py +balt src/load.py setlocal fdm=manual setlocal fde=0 setlocal fmr={{{,}}} @@ -301,19 +343,19 @@ setlocal fdn=20 setlocal fen silent! normal! zE let &fdl = &fdl -let s:l = 19 - ((6 * winheight(0) + 32) / 65) +let s:l = 45 - ((44 * winheight(0) + 32) / 64) if s:l < 1 | let s:l = 1 | endif keepjumps exe s:l normal! zt -keepjumps 19 +keepjumps 45 normal! 0 wincmd w argglobal -if bufexists(fnamemodify("tests/test_rule.py", ":p")) | buffer tests/test_rule.py | else | edit tests/test_rule.py | endif +if bufexists(fnamemodify("src/utils.py", ":p")) | buffer src/utils.py | else | edit src/utils.py | endif if &buftype ==# 'terminal' - silent file tests/test_rule.py + silent file src/utils.py endif -balt tests/fixtures/test-min-incorrect.yaml +balt src/load.py setlocal fdm=manual setlocal fde=0 setlocal fmr={{{,}}} @@ -324,27 +366,48 @@ setlocal fdn=20 setlocal fen silent! normal! zE let &fdl = &fdl -let s:l = 48 - ((30 * winheight(0) + 32) / 65) +let s:l = 49 - ((45 * winheight(0) + 32) / 64) if s:l < 1 | let s:l = 1 | endif keepjumps exe s:l normal! zt -keepjumps 48 +keepjumps 49 normal! 09| wincmd w -exe 'vert 1resize ' . ((&columns * 108 + 163) / 327) -exe 'vert 2resize ' . ((&columns * 108 + 163) / 327) -exe 'vert 3resize ' . ((&columns * 109 + 163) / 327) +argglobal +if bufexists(fnamemodify("tests/test_utils.py", ":p")) | buffer tests/test_utils.py | else | edit tests/test_utils.py | endif +if &buftype ==# 'terminal' + silent file tests/test_utils.py +endif +balt tests/test_load.py +setlocal fdm=manual +setlocal fde=0 +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=0 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +silent! normal! zE +let &fdl = &fdl +let s:l = 36 - ((35 * winheight(0) + 32) / 64) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 36 +normal! 0 +wincmd w +exe 'vert 1resize ' . ((&columns * 81 + 163) / 327) +exe 'vert 2resize ' . ((&columns * 81 + 163) / 327) +exe 'vert 3resize ' . ((&columns * 81 + 163) / 327) +exe 'vert 4resize ' . ((&columns * 81 + 163) / 327) tabnext -edit src/models/step.py +edit src/rule.py let s:save_splitbelow = &splitbelow let s:save_splitright = &splitright set splitbelow splitright wincmd _ | wincmd | vsplit -wincmd _ | wincmd | -vsplit -2wincmd h -wincmd w +1wincmd h wincmd w let &splitbelow = s:save_splitbelow let &splitright = s:save_splitright @@ -355,11 +418,10 @@ set winminheight=0 set winheight=1 set winminwidth=0 set winwidth=1 -exe 'vert 1resize ' . ((&columns * 109 + 163) / 327) -exe 'vert 2resize ' . ((&columns * 108 + 163) / 327) -exe 'vert 3resize ' . ((&columns * 108 + 163) / 327) +exe 'vert 1resize ' . ((&columns * 163 + 163) / 327) +exe 'vert 2resize ' . ((&columns * 163 + 163) / 327) argglobal -balt src/rules/step_approved.py +balt tests/test_rule.py setlocal fdm=manual setlocal fde=0 setlocal fmr={{{,}}} @@ -370,19 +432,19 @@ setlocal fdn=20 setlocal fen silent! normal! zE let &fdl = &fdl -let s:l = 33 - ((32 * winheight(0) + 32) / 65) +let s:l = 41 - ((40 * winheight(0) + 32) / 64) if s:l < 1 | let s:l = 1 | endif keepjumps exe s:l normal! zt -keepjumps 33 -normal! 044| +keepjumps 41 +normal! 0 wincmd w argglobal -if bufexists(fnamemodify("src/rules/step_approved.py", ":p")) | buffer src/rules/step_approved.py | else | edit src/rules/step_approved.py | endif +if bufexists(fnamemodify("tests/test_rule.py", ":p")) | buffer tests/test_rule.py | else | edit tests/test_rule.py | endif if &buftype ==# 'terminal' - silent file src/rules/step_approved.py + silent file tests/test_rule.py endif -balt src/models/step.py +balt tests/fixtures/test-min-incorrect.yaml setlocal fdm=manual setlocal fde=0 setlocal fmr={{{,}}} @@ -393,18 +455,18 @@ setlocal fdn=20 setlocal fen silent! normal! zE let &fdl = &fdl -let s:l = 43 - ((42 * winheight(0) + 32) / 65) +let s:l = 133 - ((56 * winheight(0) + 32) / 64) if s:l < 1 | let s:l = 1 | endif keepjumps exe s:l normal! zt -keepjumps 43 -normal! 018| +keepjumps 133 +normal! 012| wincmd w +exe 'vert 1resize ' . ((&columns * 163 + 163) / 327) +exe 'vert 2resize ' . ((&columns * 163 + 163) / 327) +tabnext +edit src/models/step.py argglobal -if bufexists(fnamemodify("tests/rules/test_step_approved.py", ":p")) | buffer tests/rules/test_step_approved.py | else | edit tests/rules/test_step_approved.py | endif -if &buftype ==# 'terminal' - silent file tests/rules/test_step_approved.py -endif balt src/rules/step_approved.py setlocal fdm=manual setlocal fde=0 @@ -416,17 +478,13 @@ setlocal fdn=20 setlocal fen silent! normal! zE let &fdl = &fdl -let s:l = 63 - ((35 * winheight(0) + 32) / 65) +let s:l = 23 - ((22 * winheight(0) + 32) / 64) if s:l < 1 | let s:l = 1 | endif keepjumps exe s:l normal! zt -keepjumps 63 +keepjumps 23 normal! 029| -wincmd w -exe 'vert 1resize ' . ((&columns * 109 + 163) / 327) -exe 'vert 2resize ' . ((&columns * 108 + 163) / 327) -exe 'vert 3resize ' . ((&columns * 108 + 163) / 327) -tabnext 1 +tabnext 2 set stal=1 if exists('s:wipebuf') && len(win_findbuf(s:wipebuf)) == 0 && getbufvar(s:wipebuf, '&buftype') isnot# 'terminal' silent exe 'bwipe ' . s:wipebuf @@ -434,8 +492,6 @@ endif unlet! s:wipebuf set winheight=1 winwidth=20 let &shortmess = s:shortmess_save -let &winminheight = s:save_winminheight -let &winminwidth = s:save_winminwidth let s:sx = expand(":p:r")."x.vim" if filereadable(s:sx) exe "source " . fnameescape(s:sx) diff --git a/lint-workflow/cli.py b/lint-workflow/cli.py index 7372f26a..0802f541 100644 --- a/lint-workflow/cli.py +++ b/lint-workflow/cli.py @@ -50,7 +50,7 @@ def main(input_args=None): args = parser.parse_args(input_args) if args.command == "lint": - return linter_cmd.run(args.files) + return linter_cmd.run(args.files, args.strict) if args.command == "actions": if args.actions_command == "add": diff --git a/lint-workflow/src/actions.py b/lint-workflow/src/actions.py index f25f9917..76937293 100644 --- a/lint-workflow/src/actions.py +++ b/lint-workflow/src/actions.py @@ -7,11 +7,26 @@ from dataclasses import asdict from typing import Union, Tuple -from src.utils import Colors, LintFinding, Settings, SettingsError, Action +from src.utils import Colors, Settings, SettingsError, Action class ActionsCmd: def __init__(self, settings: Settings = None) -> None: + """Command to manage the pre-approved list of Actions + + This class contains logic to manage the list of pre-approved actions + to include: + - updating the action data in the list + - adding a new pre-approved action to the list with the data from the + latest release + + This class also includes supporting logic to interact with GitHub + + Args: + settings: + A Settings object that contains any default, overriden, or custom settings + required anywhere in the application. + """ self.settings = settings @staticmethod @@ -20,6 +35,10 @@ def extend_parser(subparsers: argparse.ArgumentParser) -> argparse.ArgumentParse Add 'actions add' and 'actions update' to the CLI as sub-commands along with the options and arguments for each. + + Args: + subparsers: + The main argument parser to add sub commands and arguments to """ parser_actions = subparsers.add_parser("actions", help="actions help") parser_actions.add_argument( diff --git a/lint-workflow/src/lint.py b/lint-workflow/src/lint.py index 0bc37607..d4d6ded4 100644 --- a/lint-workflow/src/lint.py +++ b/lint-workflow/src/lint.py @@ -1,18 +1,26 @@ import argparse import os +from functools import reduce + from src.load import WorkflowBuilder, Rules from src.utils import Colors, LintFinding, Settings, SettingsError -PROBLEM_LEVELS = { - "warning": 1, - "error": 2, -} - - class LinterCmd: def __init__(self, settings: Settings = None) -> None: + """Command to lint GitHub Action Workflow files + + This class contains logic to lint workflows that are passed in. + Supporting logic is supplied to: + - build out the list of Rules desired + - select and validate the workflow files to lint + + Args: + settings: + A Settings object that contains any default, overriden, or custom settings + required anywhere in the application. + """ self.rules = Rules(settings=settings) @staticmethod @@ -20,6 +28,10 @@ def extend_parser(subparsers: argparse.ArgumentParser) -> argparse.ArgumentParse """Extends the CLI subparser with the options for LintCmd. Add 'lint' as a sub command along with its options and arguments + + Args: + subparsers: + The main argument parser to add sub commands and arguments to """ parser_lint = subparsers.add_parser("lint", help="lint help") parser_lint.add_argument( @@ -38,27 +50,35 @@ def extend_parser(subparsers: argparse.ArgumentParser) -> argparse.ArgumentParse return subparsers def get_max_error_level(self, findings: list[LintFinding]) -> int: - """Get max error level from list of findings.""" + """Get max error level from list of findings. + + Compute the maximum error level to determine the exit code required. + # if max(error) return exit(1); else return exit(0) + + Args: + findings: + All of the findings that the linter found while linting a workflows. + + Return: + The numeric value of the maximum lint finding + """ if len(findings) == 0: return 0 - max_problem = max(findings, key=lambda finding: PROBLEM_LEVELS[finding.level]) - max_problem_level = PROBLEM_LEVELS[max_problem.level] - return max_problem_level - - def print_finding(self, finding: LintFinding) -> None: - """Print formatted and colored finding.""" - if finding.level == "warning": - color = Colors.yellow - elif finding.level == "error": - color = Colors.red - else: - color = Colors.white + return max(findings, key=lambda finding: finding.level.code).level.code - line = f" - \033[{color}{finding.level}\033[0m {finding.description}" + def lint_file(self, filename: str) -> int: + """Lint a single workflow. - print(line) + Run all of the Workflow, Job, and Step level rules that have been enabled. - def lint_file(self, filename: str) -> int: + Args: + filename: + The name of the file that contains the workflow to lint + + Returns: + The maximum error level found in the file (none, warning, error) to + calculate the exit code from. + """ findings = [] max_error_level = 0 @@ -81,17 +101,24 @@ def lint_file(self, filename: str) -> int: if len(findings) > 0: for finding in findings: - self.print_finding(finding) + print(f" - {finding}") print() max_error_level = self.get_max_error_level(findings) return max_error_level - def generate_files(self, files: list) -> list: - """ - Takes in an argument of directory and/or files in list format from the CLI. - Returns a sorted set of all workflow files in the path(s) specified. + def generate_files(self, files: list[str]) -> list[str]: + """Generate the list of files to lint. + + Searches the list of directory and/or files taken from the CLI. + + Args: + files: + list of file names or director names. + + Returns: + A sorted set of all workflow files in the path(s) specified. """ workflow_files = [] for path in files: @@ -106,19 +133,27 @@ def generate_files(self, files: list) -> list: return sorted(set(workflow_files)) - def run(self, input_files: list[str]) -> int: + def run(self, input_files: list[str], strict: bool = False) -> int: + """Execute the LinterCmd. + + Args: + input_files: + list of file names or director names. + strict: + fail on WARNING instead of succeed + + Returns + The return_code for the entire CLI to indicate success/failure + """ files = self.generate_files(input_files) if len(input_files) > 0: - prob_levels = list(map(self.lint_file, files)) - - max_error_level = max(prob_levels) + return_code = reduce( + lambda a, b: a if a > b else b, + map(self.lint_file, files) + ) - if max_error_level == PROBLEM_LEVELS["error"]: - return_code = 2 - elif max_error_level == PROBLEM_LEVELS["warning"]: - return_code = 1 if args.strict else 0 - else: + if return_code == 1 and not strict: return_code = 0 return return_code diff --git a/lint-workflow/src/load.py b/lint-workflow/src/load.py index 61c70f4c..59af3cfa 100644 --- a/lint-workflow/src/load.py +++ b/lint-workflow/src/load.py @@ -15,17 +15,38 @@ class WorkflowBuilderError(Exception): + """Custom Exception to indicate an error with the WorkflowBuilder.""" pass class WorkflowBuilder: @classmethod def __load_workflow_from_file(cls, filename: str) -> CommentedMap: + """Load YAML from disk. + + Args: + filename: + The name of the YAML file to read. + + Returns: + A CommentedMap that contains the dict() representation of the + yaml file. It includes the comments as a part of their respective + objects (depending on their location in the file). + """ with open(filename) as file: return yaml.load(file) @classmethod def __build_workflow(cls, loaded_yaml: CommentedMap) -> Workflow: + """Parse the YAML and build out the workflow to run Rules against. + + Args: + loaded_yaml: + YAML that was loaded from either code or a file + + Returns + A Workflow to run linting Rules against + """ return Workflow.from_dict( { **loaded_yaml, @@ -49,6 +70,20 @@ def __build_workflow(cls, loaded_yaml: CommentedMap) -> Workflow: def build( cls, filename: str = None, yaml: CommentedMap = None, from_file: bool = True ) -> Workflow: + """Build a Workflow from either code or a file. + + This is a method that assists in testing by abstracting the disk IO + and allows for passing in a YAML object in code. + + Args: + filename: + The name of the file to load the YAML workflow from + yaml: + Pre-loaded YAML of a workflow + from_file: + Flag to determine if the YAML has already been loaded or needs to + be loaded from disk + """ if from_file and filename is not None: return cls.__build_workflow(cls.__load_workflow_from_file(filename)) elif not from_file and yaml is not None: @@ -65,6 +100,17 @@ class Rules: step: List[Rule] = [] def __init__(self, settings: Settings) -> None: + """A collection of all of the types of rules. + + Rules is used as a collection of which Rules apply to which parts of the + workflow. It also assists in making sure the Rules that apply to multiple + types are not skipped. + + Args: + settings: + A Settings object that contains any default, overriden, or custom settings + required anywhere in the application. + """ for rule in settings.enabled_rules: module_name = rule.split(".") module_name = ".".join(module_name[:-1]) @@ -84,6 +130,7 @@ def __init__(self, settings: Settings) -> None: print(f"Error loading: {rule}\n{err}") def list(self) -> None: + """Print the loaded Rules.""" print("===== Loaded Rules =====") print("workflow rules:") for rule in self.workflow: diff --git a/lint-workflow/src/rule.py b/lint-workflow/src/rule.py index 8d615dc4..1492f535 100644 --- a/lint-workflow/src/rule.py +++ b/lint-workflow/src/rule.py @@ -3,19 +3,43 @@ from .models.workflow import Workflow from .models.job import Job from .models.step import Step -from .utils import LintFinding, Settings +from .utils import LintFinding, LintLevels, Settings class Rule: + """Base class of a Rule to extend to create a linting Rule.""" message: str = "error" on_fail: str = "error" compatibility: List[Union[Workflow, Job, Step]] = [Workflow, Job, Step] settings: Settings = None def fn(self, obj: Union[Workflow, Job, Step]) -> bool: + """Execute the Rule (this should be overriden in the extending class. + + Args: + obj: + The object that the Rule is to be run against + + Returns: + The success/failure of the result of the Rule ran on the input. + """ return False, self.message def build_lint_message(self, message: str, obj: Union[Workflow, Job, Step]) -> str: + """Build the lint failure message. + + Build the lint failure message depending on the type of object that the + Rule is being run against. + + Args: + message: + The message body of the failure + obj: + The object the Rule is being run against + + Returns: + The type specific failure message + """ obj_type = type(obj) if obj_type == Step: @@ -26,6 +50,22 @@ def build_lint_message(self, message: str, obj: Union[Workflow, Job, Step]) -> s return f"{obj_type.__name__} => {message}" def execute(self, obj: Union[Workflow, Job, Step]) -> Union[LintFinding, None]: + """Wrapper function to execute the overriden self.fn(). + + Run the Rule against the object and return the results. The result + could be an Exception message where the Rule cannot be run against + the object for whatever reason. If an exception doesn't occur, the + result is linting success or failure. + + Args: + obj: + The object the Rule is being run against + + Returns: + A LintFinding object that contains the message to print to the user + and a LintLevel that contains the level of error to calculate the + exit code with. + """ message = None if type(obj) not in self.compatibility: @@ -34,7 +74,7 @@ def execute(self, obj: Union[Workflow, Job, Step]) -> Union[LintFinding, None]: f"{type(obj).__name__} not compatible with {type(self).__name__}", obj, ), - "error", + LintLevels.ERROR, ) try: @@ -47,7 +87,7 @@ def execute(self, obj: Union[Workflow, Job, Step]) -> Union[LintFinding, None]: self.build_lint_message( f"failed to apply {type(self).__name__}\n{err}", obj ), - "error", + LintLevels.ERROR, ) return LintFinding(self.build_lint_message(message, obj), self.on_fail) diff --git a/lint-workflow/src/rules/job_environment_prefix.py b/lint-workflow/src/rules/job_environment_prefix.py index 6266a26d..95748277 100644 --- a/lint-workflow/src/rules/job_environment_prefix.py +++ b/lint-workflow/src/rules/job_environment_prefix.py @@ -1,18 +1,41 @@ -from typing import Tuple +from typing import Union, Tuple from ..rule import Rule from ..models.job import Job -from ..utils import Settings +from ..models.workflow import Workflow +from ..models.step import Step +from ..utils import LintLevels, Settings class RuleJobEnvironmentPrefix(Rule): def __init__(self, settings: Settings = None) -> None: - self.message = f"Job Environment vars should start with and underscore:" - self.on_fail = "error" - self.compatibility = [Job] - self.settings = settings + self.message: str = f"Job Environment vars should start with and underscore:" + self.on_fail: LintLevels = LintLevels.ERROR + self.compatibility: List[Union[Workflow, Job, Step]] = [Job] + self.settings: Settings = settings def fn(self, obj: Job) -> Tuple[bool, str]: + """Enforces the underscore prefix standard on job envs. + + Example: + --- + on: + workflow_dispatch: + + jobs: + job-key: + runs-on: ubuntu-22.04 + env: + _TEST_ENV: "test" + steps: + - run: echo test + + All keys under jobs.job-key.env should be prefixed with an underscore + as in _TEST_ENV. + + See tests/rules/test_job_environment_prefix.py for examples of + incorrectly names environment variables. + """ correct = True offending_keys = [] diff --git a/lint-workflow/src/rules/name_capitalized.py b/lint-workflow/src/rules/name_capitalized.py index 4926954e..e66417c0 100644 --- a/lint-workflow/src/rules/name_capitalized.py +++ b/lint-workflow/src/rules/name_capitalized.py @@ -4,16 +4,39 @@ from ..models.workflow import Workflow from ..models.job import Job from ..models.step import Step -from ..utils import Settings +from ..utils import LintLevels, Settings class RuleNameCapitalized(Rule): def __init__(self, settings: Settings = None) -> None: self.message = "name must capitalized" - self.on_fail = "error" - self.settings = settings + self.on_fail: LintLevels = LintLevels.ERROR + self.settings: Settings = settings def fn(self, obj: Union[Workflow, Job, Step]) -> Tuple[bool, str]: + """Enforces capitalization of the first letter of any name key. + + Example: + --- + name: Test Workflow + + on: + workflow_dispatch: + + jobs: + job-key: + name: Test + runs-on: ubuntu-latest + steps: + - name: Test + run: echo test + + 'Test Workflow', 'Test', and 'Test' all start with a capital letter. + + See tests/rules/test_name_capitalized.py for examples of incorrectly + capitalized names. This Rule DOES NOT enforce that the name exists. + It only enforces capitalization IF it does. + """ if obj.name: return obj.name[0].isupper(), self.message return True, "" # Force passing if obj.name doesn't exist diff --git a/lint-workflow/src/rules/name_exists.py b/lint-workflow/src/rules/name_exists.py index 8fe3e60a..165038b0 100644 --- a/lint-workflow/src/rules/name_exists.py +++ b/lint-workflow/src/rules/name_exists.py @@ -4,16 +4,38 @@ from ..models.workflow import Workflow from ..models.job import Job from ..models.step import Step -from ..utils import Settings +from ..utils import LintLevels, Settings class RuleNameExists(Rule): def __init__(self, settings: Settings = None) -> None: self.message = "name must exist" - self.on_fail = "error" - self.settings = settings + self.on_fail: LintLevels = LintLevels.ERROR + self.settings: Settings = settings def fn(self, obj: Union[Workflow, Job, Step]) -> Tuple[bool, str]: + """Enforces the existence of names. + + Example: + --- + name: Test Workflow + + on: + workflow_dispatch: + + jobs: + job-key: + name: Test + runs-on: ubuntu-latest + steps: + - name: Test + run: echo test + + 'Test Workflow', 'Test', and 'Test' all exist. + + See tests/rules/test_name_exists.py for examples where a name does not + exist. + """ if obj.name is not None: return True, "" return False, self.message diff --git a/lint-workflow/src/rules/pinned_job_runner.py b/lint-workflow/src/rules/pinned_job_runner.py index d4ce92a9..d2897f48 100644 --- a/lint-workflow/src/rules/pinned_job_runner.py +++ b/lint-workflow/src/rules/pinned_job_runner.py @@ -1,18 +1,35 @@ -from typing import Tuple +from typing import Union, Tuple from ..rule import Rule from ..models.job import Job -from ..utils import Settings +from ..models.workflow import Workflow +from ..models.step import Step +from ..utils import LintLevels, Settings class RuleJobRunnerVersionPinned(Rule): def __init__(self, settings: Settings = None) -> None: self.message = "Workflow runner must be pinned" - self.on_fail = "error" - self.compatibility = [Job] - self.settings = settings + self.on_fail: LintLevels = LintLevels.ERROR + self.compatibility: List[Union[Workflow, Job, Step]] = [Job] + self.settings: Settings = settings def fn(self, obj: Job) -> Tuple[bool, str]: + """Enforces runners are pinned to a version + + Example: + --- + on: + workflow_dispatch: + + jobs: + job-key: + runs-on: ubuntu-22.04 + steps: + - run: echo test + + 'runs-on' is pinned to '22.04' instead of 'latest' + """ if "latest" not in obj.runs_on: return True, "" return False, self.message diff --git a/lint-workflow/src/rules/step_approved.py b/lint-workflow/src/rules/step_approved.py index 9cfe4ad9..88a6673b 100644 --- a/lint-workflow/src/rules/step_approved.py +++ b/lint-workflow/src/rules/step_approved.py @@ -1,18 +1,25 @@ -from typing import Tuple +from typing import Union, Tuple from ..rule import Rule +from ..models.job import Job +from ..models.workflow import Workflow from ..models.step import Step -from ..utils import Settings +from ..utils import LintLevels, Settings class RuleStepUsesApproved(Rule): def __init__(self, settings: Settings = None) -> None: self.message = f"error" - self.on_fail = "warn" - self.compatibility = [Step] - self.settings = settings + self.on_fail: LintLevels = LintLevels.WARNING + self.compatibility: List[Union[Workflow, Job, Step]] = [Step] + self.settings: Settings = settings - def force_pass(self, obj: Step) -> bool: + def skip(self, obj: Step) -> bool: + """Skip this Rule on some Steps. + + This Rule does not apply to a few types of Steps. These + Rules are skipped. + """ ## Force pass for any shell steps if not obj.uses: return True, "" @@ -28,7 +35,38 @@ def force_pass(self, obj: Step) -> bool: return False def fn(self, obj: Step) -> Tuple[bool, str]: - if self.force_pass(obj): + """Enforces all externally used Actions are on the pre-approved list. + + The pre-approved list allows tight auditing on what Actions are trusted + and allowed to be run in our environments. This helps mitigate risks + against supply chain attacks in our pipelines. + + Example: + --- + on: + workflow_dispatch: + + jobs: + job-key: + runs-on: ubuntu-22.04 + steps: + - name: Checkout Branch + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Test Bitwarden Action + uses: bitwarden/gh-actions/get-keyvault-secrets@main + + - name: Test Local Action + uses: ./actions/test-action + + - name: Test Run Action + run: echo "test" + + In this example, 'actions/checkout' must be on the pre-approved list + and the metadata must match in order to succeed. The other three + Steps will be skipped. + """ + if self.skip(obj): return True, "" path, hash = obj.uses.split("@") diff --git a/lint-workflow/src/rules/step_pinned.py b/lint-workflow/src/rules/step_pinned.py index 2899b9ee..a2fe2eee 100644 --- a/lint-workflow/src/rules/step_pinned.py +++ b/lint-workflow/src/rules/step_pinned.py @@ -1,18 +1,25 @@ -from typing import Tuple +from typing import Union, Tuple from ..rule import Rule +from ..models.job import Job +from ..models.workflow import Workflow from ..models.step import Step -from ..utils import Settings +from ..utils import LintLevels, Settings class RuleStepUsesPinned(Rule): def __init__(self, settings: Settings = None) -> None: self.message = f"error" - self.on_fail = "error" - self.compatibility = [Step] - self.settings = settings + self.on_fail: LintLevels = LintLevels.ERROR + self.compatibility: List[Union[Workflow, Job, Step]] = [Step] + self.settings: Settings = settings - def force_pass(self, obj: Step) -> bool: + def skip(self, obj: Step) -> bool: + """Skip this Rule on some Steps. + + This Rule does not apply to a few types of Steps. These + Rules are skipped. + """ if not obj.uses: return True, "" @@ -23,7 +30,33 @@ def force_pass(self, obj: Step) -> bool: return False def fn(self, obj: Step) -> Tuple[bool, str]: - if self.force_pass(obj): + """Enforces all Actions to be pinned in a specific way. + + Pinning external Action hashes prevents unknown updates that could + break the pipelines or be the entry point to a supply chain attack. + + Pinning internal Actions to branches allow for less updates as work + is done on those repos. This is mainly to support our Action + monorepo architecture of our Actions. + + Example: + - name: Checkout Branch + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Test Bitwarden Action + uses: bitwarden/gh-actions/get-keyvault-secrets@main + + - name: Test Local Action + uses: ./actions/test-action + + - name: Test Run Action + run: echo "test" + + In this example, 'actions/checkout' must be pinned to the full commit + of the tag while 'bitwarden/gh-actions/get-keyvault-secrets' must be + pinned to 'main'. The other two Steps will be skipped. + """ + if self.skip(obj): return True, "" path, ref = obj.uses.split("@") diff --git a/lint-workflow/src/utils.py b/lint-workflow/src/utils.py index 74f8d34e..77b6ae47 100644 --- a/lint-workflow/src/utils.py +++ b/lint-workflow/src/utils.py @@ -1,4 +1,5 @@ -from dataclasses import dataclass +from dataclasses import asdict, dataclass +from enum import Enum from typing import Self @@ -17,20 +18,56 @@ class Colors: @dataclass +class LintLevel: + """Class to contain the numeric level and color of linting.""" + code: int + color: Colors + + +class LintLevels(LintLevel, Enum): + """Collection of the different types of LintLevels available.""" + NONE = 0, Colors.white + WARNING = 1, Colors.yellow + ERROR = 2, Colors.red + + class LintFinding: - """Represents a linting problem.""" + """Represents a problem detected by linting.""" + + def __init__( + self, + description: str = "", + level: LintLevel = None + ) -> None: + self.description = description + self.level = level - description: str = "" - level: str = None + def __str__(self) -> str: + """String representation of the class. + + Returns: + String representation of itself. + """ + return f"\033[{self.level.color}{self.level.name.lower()}\033[0m {self.description}" @dataclass class Action: + """Collection of the metadata associated with a GitHub Action.""" name: str version: str = "" sha: str = "" def __eq__(self, other: Self) -> bool: + """Override Action equality. + + Args: + other: + Another Action type object to compare + + Return + The state of eqaulity + """ return ( self.name == other.name and self.version == other.version @@ -38,23 +75,42 @@ def __eq__(self, other: Self) -> bool: ) def __ne__(self, other: Self) -> bool: + """Override Action unequality. + + Args: + other: + Another Action type object to compare + + Return + The negation of the state of eqaulity + """ return not self.__eq__(other) class SettingsError(Exception): + """Custom Exception to indicate an error with loading Settings.""" pass -@dataclass class Settings: enabled_rules: list[str] approved_actions: dict[str, Action] def __init__( self, - enabled_rules: list[str] = None, - approved_actions: dict[str, dict[str, str]] = None, + enabled_rules: list[str] = [], + approved_actions: dict[str, dict[str, str]] = {}, ): + """Settings object that can be overriden in settings.py. + + Args: + enabled_rules: + All of the python modules that implement a Rule to be run against + the workflows. These must be available somewhere on the PYTHONPATH + approved_actions: + The colleciton of GitHub Actions that are pre-approved to be used + in any workflow (Required by src.rules.step_approved) + """ self.enabled_rules = enabled_rules self.approved_actions = { name: Action(**action) for name, action in approved_actions.items() diff --git a/lint-workflow/tests/test_lint.py b/lint-workflow/tests/test_lint.py new file mode 100644 index 00000000..277aa374 --- /dev/null +++ b/lint-workflow/tests/test_lint.py @@ -0,0 +1,34 @@ +import json +import pytest + +from .conftest import FIXTURE_DIR +from .context import src + +from src.lint import LinterCmd +from src.utils import Settings, LintFinding, LintLevels + + +@pytest.fixture +def settings(): + return Settings() + + +def test_get_max_error_level(settings): + linter = LinterCmd(settings=settings) + + assert linter.get_max_error_level([ + LintFinding(level=LintLevels.WARNING), + LintFinding(level=LintLevels.WARNING) + ]) == 1 + + assert linter.get_max_error_level([ + LintFinding(level=LintLevels.ERROR), + LintFinding(level=LintLevels.ERROR) + ]) == 2 + + assert linter.get_max_error_level([ + LintFinding(level=LintLevels.ERROR), + LintFinding(level=LintLevels.ERROR), + LintFinding(level=LintLevels.WARNING), + LintFinding(level=LintLevels.WARNING) + ]) == 2 diff --git a/lint-workflow/tests/test_utils.py b/lint-workflow/tests/test_utils.py index e9b667c8..77830eb7 100644 --- a/lint-workflow/tests/test_utils.py +++ b/lint-workflow/tests/test_utils.py @@ -7,7 +7,7 @@ from .conftest import FIXTURE_DIR from .context import src -from src.utils import Action +from src.utils import Action, Colors, LintFinding, LintLevels def test_action_eq(): @@ -38,3 +38,17 @@ def test_action_ne(): assert (action_a == action_b) == False assert (action_a != action_b) == True + + +def test_lint_level(): + warning = LintLevels.WARNING + assert warning.code == 1 + assert warning.color == Colors.yellow + + +def test_lint_finding(): + warning = LintFinding(level=LintLevels.WARNING) + assert str(warning) == '\x1b[33mwarning\x1b[0m ' + + error = LintFinding(level=LintLevels.ERROR) + assert str(error) == '\x1b[31merror\x1b[0m ' From 060a33d1dfcb3324954dbe981e868356ecab6b00 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Mon, 8 Jan 2024 16:24:29 -0800 Subject: [PATCH 15/57] Run black over the code --- lint-workflow/src/actions.py | 1 - lint-workflow/src/lint.py | 5 +-- lint-workflow/src/load.py | 1 + lint-workflow/src/rule.py | 1 + lint-workflow/src/rules/step_approved.py | 2 +- lint-workflow/src/rules/step_pinned.py | 4 +- lint-workflow/src/utils.py | 8 ++-- .../tests/rules/test_name_capitalized.py | 1 - lint-workflow/tests/test_lint.py | 44 ++++++++++++------- lint-workflow/tests/test_utils.py | 22 +++------- 10 files changed, 45 insertions(+), 44 deletions(-) diff --git a/lint-workflow/src/actions.py b/lint-workflow/src/actions.py index 76937293..4cb867d6 100644 --- a/lint-workflow/src/actions.py +++ b/lint-workflow/src/actions.py @@ -187,4 +187,3 @@ def update(self, filename: str) -> None: updated_actions[action.name] = latest_release self.save_actions(updated_actions, filename) - diff --git a/lint-workflow/src/lint.py b/lint-workflow/src/lint.py index d4d6ded4..5aea8aa3 100644 --- a/lint-workflow/src/lint.py +++ b/lint-workflow/src/lint.py @@ -11,7 +11,7 @@ class LinterCmd: def __init__(self, settings: Settings = None) -> None: """Command to lint GitHub Action Workflow files - This class contains logic to lint workflows that are passed in. + This class contains logic to lint workflows that are passed in. Supporting logic is supplied to: - build out the list of Rules desired - select and validate the workflow files to lint @@ -149,8 +149,7 @@ def run(self, input_files: list[str], strict: bool = False) -> int: if len(input_files) > 0: return_code = reduce( - lambda a, b: a if a > b else b, - map(self.lint_file, files) + lambda a, b: a if a > b else b, map(self.lint_file, files) ) if return_code == 1 and not strict: diff --git a/lint-workflow/src/load.py b/lint-workflow/src/load.py index 59af3cfa..1dda9f71 100644 --- a/lint-workflow/src/load.py +++ b/lint-workflow/src/load.py @@ -16,6 +16,7 @@ class WorkflowBuilderError(Exception): """Custom Exception to indicate an error with the WorkflowBuilder.""" + pass diff --git a/lint-workflow/src/rule.py b/lint-workflow/src/rule.py index 1492f535..791c105b 100644 --- a/lint-workflow/src/rule.py +++ b/lint-workflow/src/rule.py @@ -8,6 +8,7 @@ class Rule: """Base class of a Rule to extend to create a linting Rule.""" + message: str = "error" on_fail: str = "error" compatibility: List[Union[Workflow, Job, Step]] = [Workflow, Job, Step] diff --git a/lint-workflow/src/rules/step_approved.py b/lint-workflow/src/rules/step_approved.py index 88a6673b..b184059b 100644 --- a/lint-workflow/src/rules/step_approved.py +++ b/lint-workflow/src/rules/step_approved.py @@ -63,7 +63,7 @@ def fn(self, obj: Step) -> Tuple[bool, str]: run: echo "test" In this example, 'actions/checkout' must be on the pre-approved list - and the metadata must match in order to succeed. The other three + and the metadata must match in order to succeed. The other three Steps will be skipped. """ if self.skip(obj): diff --git a/lint-workflow/src/rules/step_pinned.py b/lint-workflow/src/rules/step_pinned.py index a2fe2eee..2f49cfd7 100644 --- a/lint-workflow/src/rules/step_pinned.py +++ b/lint-workflow/src/rules/step_pinned.py @@ -36,7 +36,7 @@ def fn(self, obj: Step) -> Tuple[bool, str]: break the pipelines or be the entry point to a supply chain attack. Pinning internal Actions to branches allow for less updates as work - is done on those repos. This is mainly to support our Action + is done on those repos. This is mainly to support our Action monorepo architecture of our Actions. Example: @@ -52,7 +52,7 @@ def fn(self, obj: Step) -> Tuple[bool, str]: - name: Test Run Action run: echo "test" - In this example, 'actions/checkout' must be pinned to the full commit + In this example, 'actions/checkout' must be pinned to the full commit of the tag while 'bitwarden/gh-actions/get-keyvault-secrets' must be pinned to 'main'. The other two Steps will be skipped. """ diff --git a/lint-workflow/src/utils.py b/lint-workflow/src/utils.py index 77b6ae47..18e2b11a 100644 --- a/lint-workflow/src/utils.py +++ b/lint-workflow/src/utils.py @@ -20,12 +20,14 @@ class Colors: @dataclass class LintLevel: """Class to contain the numeric level and color of linting.""" + code: int color: Colors class LintLevels(LintLevel, Enum): """Collection of the different types of LintLevels available.""" + NONE = 0, Colors.white WARNING = 1, Colors.yellow ERROR = 2, Colors.red @@ -35,9 +37,7 @@ class LintFinding: """Represents a problem detected by linting.""" def __init__( - self, - description: str = "", - level: LintLevel = None + self, description: str = "", level: LintLevel = None ) -> None: self.description = description self.level = level @@ -54,6 +54,7 @@ def __str__(self) -> str: @dataclass class Action: """Collection of the metadata associated with a GitHub Action.""" + name: str version: str = "" sha: str = "" @@ -89,6 +90,7 @@ def __ne__(self, other: Self) -> bool: class SettingsError(Exception): """Custom Exception to indicate an error with loading Settings.""" + pass diff --git a/lint-workflow/tests/rules/test_name_capitalized.py b/lint-workflow/tests/rules/test_name_capitalized.py index 12acf926..1a4f3849 100644 --- a/lint-workflow/tests/rules/test_name_capitalized.py +++ b/lint-workflow/tests/rules/test_name_capitalized.py @@ -66,7 +66,6 @@ def missing_name_workflow(): return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) - @pytest.fixture def rule(): return RuleNameCapitalized() diff --git a/lint-workflow/tests/test_lint.py b/lint-workflow/tests/test_lint.py index 277aa374..eee5bddf 100644 --- a/lint-workflow/tests/test_lint.py +++ b/lint-workflow/tests/test_lint.py @@ -16,19 +16,31 @@ def settings(): def test_get_max_error_level(settings): linter = LinterCmd(settings=settings) - assert linter.get_max_error_level([ - LintFinding(level=LintLevels.WARNING), - LintFinding(level=LintLevels.WARNING) - ]) == 1 - - assert linter.get_max_error_level([ - LintFinding(level=LintLevels.ERROR), - LintFinding(level=LintLevels.ERROR) - ]) == 2 - - assert linter.get_max_error_level([ - LintFinding(level=LintLevels.ERROR), - LintFinding(level=LintLevels.ERROR), - LintFinding(level=LintLevels.WARNING), - LintFinding(level=LintLevels.WARNING) - ]) == 2 + assert ( + linter.get_max_error_level( + [ + LintFinding(level=LintLevels.WARNING), + LintFinding(level=LintLevels.WARNING), + ] + ) + == 1 + ) + + assert ( + linter.get_max_error_level( + [LintFinding(level=LintLevels.ERROR), LintFinding(level=LintLevels.ERROR)] + ) + == 2 + ) + + assert ( + linter.get_max_error_level( + [ + LintFinding(level=LintLevels.ERROR), + LintFinding(level=LintLevels.ERROR), + LintFinding(level=LintLevels.WARNING), + LintFinding(level=LintLevels.WARNING), + ] + ) + == 2 + ) diff --git a/lint-workflow/tests/test_utils.py b/lint-workflow/tests/test_utils.py index 77830eb7..d8756882 100644 --- a/lint-workflow/tests/test_utils.py +++ b/lint-workflow/tests/test_utils.py @@ -11,11 +11,7 @@ def test_action_eq(): - action_def = { - "name": "bitwarden/sm-action", - "version": "1.0.0", - "sha": "some-sha" - } + action_def = {"name": "bitwarden/sm-action", "version": "1.0.0", "sha": "some-sha"} action_a = Action(**action_def) action_b = Action(**action_def) @@ -25,16 +21,8 @@ def test_action_eq(): def test_action_ne(): - action_a = Action( - name = "bitwarden/sm-action", - version = "1.0.0", - sha = "some-sha" - ) - action_b = Action( - name = "bitwarden/sm-action", - version = "1.1.0", - sha = "some-other-sha" - ) + action_a = Action(name="bitwarden/sm-action", version="1.0.0", sha="some-sha") + action_b = Action(name="bitwarden/sm-action", version="1.1.0", sha="some-other-sha") assert (action_a == action_b) == False assert (action_a != action_b) == True @@ -48,7 +36,7 @@ def test_lint_level(): def test_lint_finding(): warning = LintFinding(level=LintLevels.WARNING) - assert str(warning) == '\x1b[33mwarning\x1b[0m ' + assert str(warning) == "\x1b[33mwarning\x1b[0m " error = LintFinding(level=LintLevels.ERROR) - assert str(error) == '\x1b[31merror\x1b[0m ' + assert str(error) == "\x1b[31merror\x1b[0m " From 33a9e7416aeedbe2289cae2cfc500624dae6b061 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Mon, 8 Jan 2024 16:27:52 -0800 Subject: [PATCH 16/57] Clean up the unused tests --- lint-workflow/tests/OLD_test_action_update.py | 20 ------ lint-workflow/tests/OLD_test_lint.py | 23 ------- lint-workflow/tests/OLD_test_main.py | 66 ------------------- .../tests/OLD_test_workflow_files.py | 37 ----------- 4 files changed, 146 deletions(-) delete mode 100644 lint-workflow/tests/OLD_test_action_update.py delete mode 100644 lint-workflow/tests/OLD_test_lint.py delete mode 100644 lint-workflow/tests/OLD_test_main.py delete mode 100644 lint-workflow/tests/OLD_test_workflow_files.py diff --git a/lint-workflow/tests/OLD_test_action_update.py b/lint-workflow/tests/OLD_test_action_update.py deleted file mode 100644 index b2c12e6f..00000000 --- a/lint-workflow/tests/OLD_test_action_update.py +++ /dev/null @@ -1,20 +0,0 @@ -import pytest -import urllib3 as urllib - -from lint import get_action_update, memoized_action_update_urls - -http = urllib.PoolManager() - - -@pytest.mark.skip() -def test_action_update(): - action_id = "actions/checkout@86f86b36ef15e6570752e7175f451a512eac206b" - sub_string = "github.com" - update_url = get_action_update(action_id) - assert str(sub_string) in str(update_url) - - r = http.request("GET", update_url) - - assert r.status == 200 - - assert "actions/checkout" in memoized_action_update_urls diff --git a/lint-workflow/tests/OLD_test_lint.py b/lint-workflow/tests/OLD_test_lint.py deleted file mode 100644 index 4f89a084..00000000 --- a/lint-workflow/tests/OLD_test_lint.py +++ /dev/null @@ -1,23 +0,0 @@ -import pytest - -from lint import lint -from .configs import FIXTURES_DIR - - -@pytest.mark.skip() -def test_lint(capfd): - file_path = f"{FIXTURES_DIR}/test.yml" - lint_output = lint(file_path) - out, err = capfd.readouterr() - assert ( - "\x1b[33mwarning\x1b[0m Name value for workflow is not capitalized. [crowdin Pull]" - in out - ) - assert ( - "\x1b[33mwarning\x1b[0m Step 4 of job key 'crowdin-pull' uses an outdated action, consider updating it" - in out - ) - assert ( - "\x1b[31merror\x1b[0m Step 2 of job key 'crowdin-pull' uses an non-existing action: Azure/logi@77f1b2e3fb80c0e8645114159d17008b8a2e475a." - in out - ) diff --git a/lint-workflow/tests/OLD_test_main.py b/lint-workflow/tests/OLD_test_main.py deleted file mode 100644 index bc4f863a..00000000 --- a/lint-workflow/tests/OLD_test_main.py +++ /dev/null @@ -1,66 +0,0 @@ -import pytest - -from lint import main -from .configs import FIXTURES_DIR - -# Tests for argparse inputs and outputs using capsys.readouterr() - -FIXTURES_DIR = "./tests/fixtures" - - -@pytest.mark.skip() -def test_main_single_file(capsys): - main([f"{FIXTURES_DIR}/test.yml"]) - captured = capsys.readouterr() - result = captured.out - assert "test.yml" in result - - -@pytest.mark.skip() -def test_main_multiple_files(capsys): - main([f"{FIXTURES_DIR}/test.yml {FIXTURES_DIR}/test-alt.yml"]) - captured = capsys.readouterr() - result = captured.out - assert isinstance(result, str) - assert "test.yml" in result - assert "test-alt.yml" in result - - -@pytest.mark.skip() -def test_main_folder(capsys): - main([f"{FIXTURES_DIR}"]) - captured = capsys.readouterr() - result = captured.out - assert isinstance(result, str) - assert "test.yml" in result - assert "test-alt.yml" in result - - -@pytest.mark.skip() -def test_main_folder_and_files(capsys): - main([f"{FIXTURES_DIR}/test.yml {FIXTURES_DIR}"]) - captured = capsys.readouterr() - result = captured.out - print(result) - - -@pytest.mark.skip() -def test_main_not_found(capsys): - # File that doesn't exist - main(["not-a-real-file.yml"]) - captured = capsys.readouterr() - result = captured.out - assert isinstance(result, str) - assert 'File(s)/Directory: "not-a-real-file.yml" does not exist, exiting.' in result - # Empty string - main([""]) - captured = capsys.readouterr() - result = captured.out - assert isinstance(result, str) - assert 'File(s)/Directory: "" does not exist, exiting.' in result - # Spaces in string - main([" "]) - captured = capsys.readouterr() - result = captured.out - assert isinstance(result, str) - assert 'File(s)/Directory: " " does not exist, exiting.' in result diff --git a/lint-workflow/tests/OLD_test_workflow_files.py b/lint-workflow/tests/OLD_test_workflow_files.py deleted file mode 100644 index 6d969306..00000000 --- a/lint-workflow/tests/OLD_test_workflow_files.py +++ /dev/null @@ -1,37 +0,0 @@ -import os -import pytest - -from lint import workflow_files -from .configs import FIXTURES_DIR - - -@pytest.mark.skip() -def test_workflow_files(): - assert workflow_files("") == [] - assert workflow_files("not-a-real-file.yml") == [] - assert workflow_files(f"{FIXTURES_DIR}/test.yml") == [f"{FIXTURES_DIR}/test.yml"] - # multiple files - assert workflow_files( - f"{FIXTURES_DIR}/test.yml {FIXTURES_DIR}/test-alt.yml" - ) == sorted([f"{FIXTURES_DIR}/test.yml", f"{FIXTURES_DIR}/test-alt.yml"]) - # directory - assert workflow_files(FIXTURES_DIR) == sorted( - set( - [ - f"{FIXTURES_DIR}/{file}" - for file in os.listdir(FIXTURES_DIR) - if file.endswith((".yml", ".yaml")) - ] - ) - ) - # directory and files - assert workflow_files(f"{FIXTURES_DIR}/test.yml {FIXTURES_DIR}") == sorted( - set( - [f"{FIXTURES_DIR}/test.yml"] - + [ - f"{FIXTURES_DIR}/{file}" - for file in os.listdir(FIXTURES_DIR) - if file.endswith((".yml", ".yaml")) - ] - ) - ) From d35110c4605b07811787f31250492e8f5b30b22c Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Tue, 9 Jan 2024 11:02:25 -0800 Subject: [PATCH 17/57] Expanded docs on how to add a new Rule --- lint-workflow/README.md | 101 +++++++++++++++++++++++++---------- lint-workflow/src/actions.py | 5 +- lint-workflow/src/lint.py | 5 +- 3 files changed, 81 insertions(+), 30 deletions(-) diff --git a/lint-workflow/README.md b/lint-workflow/README.md index 864cab1c..782adacb 100644 --- a/lint-workflow/README.md +++ b/lint-workflow/README.md @@ -1,5 +1,26 @@ # lint-workflow +## Usage + +There is currently NO packaging or distribution of this CLI tool. Until such time, the `cli.py` file needs to be run +with python 3.11+. + +`python cli.py --help` + + +``` +usage: workflow-linter [-h] [-v] {lint,actions} ... + +positional arguments: + {lint,actions} + lint Verify that a GitHub Action Workflow follows all of the Rules. + actions Add or Update Actions in the pre-approved list. + +options: + -h, --help show this help message and exit + -v, --verbose +``` + ## Development ### Requirements @@ -15,54 +36,78 @@ pipenv shell ### Testing +All built-in `src/rules` should have 100% code coverage and we should shoot for an overall coverage of 80%+. +We are lax on the +[imperative shell](https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell) +(code interacting with other systems; ie. disk, network, etc), but we strive to maintain a high coverage over the +funcationl core (objects and models). + ``` pipenv shell -pytest tests +pytest tests --cov=src ``` ### Code Reformatting +We strictly adhere to PEP8 and use `black` to maintain this adherence. `black` should be run on any change being merged +to `main`. + ``` pipenv shell black . ``` +### Add a new Rule + +A new Rule is created by extending the Rule base class and overriding the `fn(obj: Union[Workflow, Job, Step])` method. +Available attributes of `Workflows`, `Jobs` and `Steps` can be found in their definitons under `src/models`. -## Design -### Objects +For a simple example, we'll take a look at enforcing the existence of the `name` key in a Job. This is already done by +default with the src.rules.name_exists.RuleNameExists, but provides a simple enough example to walk through. -**Workflow:** -**Jobs:** -**Steps (run || uses):** +```python +from typing import Union, Tuple +from ..rule import Rule +from ..models.job import Job +from ..models.workflow import Workflow +from ..models.step import Step +from ..utils import LintLevels, Settings -### Rules -#### workflows +class RuleJobNameExists(Rule): + def __init__(self, settings: Settings = None) -> None: + self.message = "name must exist" + self.on_fail: LintLevels = LintLevels.ERROR + self.compatibility: List[Union[Workflow, Job, Step]] = [Job] + self.settings: Settings = settings -- [x] assert name exists -- [x] assert name is capitalized + def fn(self, obj: Job) -> Tuple[bool, str]: + """ """ + if obj.name is not None: + return True, "" + return False, self.message +``` -#### jobs +[TODO: Is this enough documentation on how to use?] -- [x] assert name exists -- [x] assert name is capitalized -- [x] assert runs-on is pinned -- [x] assert any environment variables start with "_" +By default, a new Rule needs five things: -#### shared steps -- [x] assert name exists -- [x] assert name is capitilized +- `self.message`: The message to return to the user on a lint failure +- `self.on_fail`: The level of failure on a lint failure (NONE, WARNING, ERROR). + NONE and WARNING will exit with a code of 0 (unless using `strict` mode for WARNING). + ERROR will exit with a non-zero exit code +- `self.compatibility`: The list of objects this rule is compatible with. This is used to create separate instances of + the Rule for each object in the Rules collection. +- `self.settings`: In general, this should default to what is shown here, but allows for overrides +- `self.fn`: The function doing the actual work to check the object and enforce the standardenforcing. -#### uses steps -- [x] assert valid hash format - correct length -- [x] assert valid hash format - cast to hexidecimal -- [x] warn using an unapproved Action -- [x] warn out of date Action -- [ ] (DEPRECATED) assert action exists in GitHub (deprecated in favor of the approved list of actions) -- [ ] (DEPRECATED) assert valid action repo path format (deprecated in favor fo the approved list of actions) +`fn` can be as simple or as complex as it needs to be to run a check on a _single_ object. This linter currently does +not support Rules that check against multiple objects at a time OR file level formatting (one empty between each step or +two empty lines between each job) -#### run steps -- [ ] assert correct format for single line run -``` + +### ToDo + +- [ ] Add Rule to assert correct format for single line run diff --git a/lint-workflow/src/actions.py b/lint-workflow/src/actions.py index 4cb867d6..6de78cf1 100644 --- a/lint-workflow/src/actions.py +++ b/lint-workflow/src/actions.py @@ -40,7 +40,10 @@ def extend_parser(subparsers: argparse.ArgumentParser) -> argparse.ArgumentParse subparsers: The main argument parser to add sub commands and arguments to """ - parser_actions = subparsers.add_parser("actions", help="actions help") + parser_actions = subparsers.add_parser( + "actions", + help="Add or Update Actions in the pre-approved list." + ) parser_actions.add_argument( "-o", "--output", action="store", default="actions.json" ) diff --git a/lint-workflow/src/lint.py b/lint-workflow/src/lint.py index 5aea8aa3..647ab28f 100644 --- a/lint-workflow/src/lint.py +++ b/lint-workflow/src/lint.py @@ -33,7 +33,10 @@ def extend_parser(subparsers: argparse.ArgumentParser) -> argparse.ArgumentParse subparsers: The main argument parser to add sub commands and arguments to """ - parser_lint = subparsers.add_parser("lint", help="lint help") + parser_lint = subparsers.add_parser( + "lint", + help="Verify that a GitHub Action Workflow follows all of the Rules." + ) parser_lint.add_argument( "-s", "--strict", From 1029baafd07909016535f83b817a06344774c944 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Wed, 10 Jan 2024 12:41:16 -0800 Subject: [PATCH 18/57] Remove old linter --- lint-workflow/lint.py | 423 ------------------------------------------ 1 file changed, 423 deletions(-) delete mode 100644 lint-workflow/lint.py diff --git a/lint-workflow/lint.py b/lint-workflow/lint.py deleted file mode 100644 index 097fc4db..00000000 --- a/lint-workflow/lint.py +++ /dev/null @@ -1,423 +0,0 @@ -import sys -import argparse -import os -import yaml -import json -import urllib3 as urllib -import logging - - -PROBLEM_LEVELS = { - "warning": 1, - "error": 2, -} - -memoized_action_update_urls = {} - - -class Colors: - """Class containing color codes for printing strings to output.""" - - black = "30m" - red = "31m" - green = "32m" - yellow = "33m" - blue = "34m" - magenta = "35m" - cyan = "36m" - white = "37m" - - -class LintFinding: - """Represents a linting problem.""" - - description: str = "" - level: str = None - - -def get_max_error_level(findings): - """Get max error level from list of findings.""" - if len(findings) == 0: - return 0 - max_problem = max(findings, key=lambda finding: PROBLEM_LEVELS[finding.level]) - max_problem_level = PROBLEM_LEVELS[max_problem.level] - return max_problem_level - - -def print_finding(finding: LintFinding): - """Print formatted and colored finding.""" - if finding.level == "warning": - color = Colors.yellow - elif finding.level == "error": - color = Colors.red - else: - color = Colors.white - - line = f" - \033[{color}{finding.level}\033[0m {finding.description}" - - print(line) - - -def get_github_api_response(url, action_id): - """Call GitHub API with error logging without throwing an exception.""" - http = urllib.PoolManager() - headers = {"user-agent": "bw-linter"} - - if os.getenv("GITHUB_TOKEN", None): - headers["Authorization"] = f"Token {os.environ['GITHUB_TOKEN']}" - - response = http.request("GET", url, headers=headers) - - if response.status == 403 and response.reason == "rate limit exceeded": - logging.error( - f"Failed to call GitHub API for action: {action_id} due to rate limit exceeded." - ) - return None - - if response.status == 401 and response.reason == "Unauthorized": - logging.error( - f"Failed to call GitHub API for action: {action_id}: {response.data}." - ) - return None - - return response - - -def action_repo_exists(action_id): - """ - Takes and action id and checks if the action repo exists. - - Example action_id: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945 - """ - - if "./" in action_id: - # Handle local workflow calls, return None since there will be no updates. - return True - - path, *hash = action_id.split("@") - - if "bitwarden" in path: - path_list = path.split("/", 2) - url = f"https://api.github.com/repos/{path_list[0]}/{path_list[1]}" - response = get_github_api_response(url, action_id) - - else: - response = get_github_api_response( - f"https://api.github.com/repos/{path}", action_id - ) - - if response is None: - # Handle github api limit exceed by returning that the action exists without actually checking - # to prevent false errors on linter output. Only show it as an linter error. - return True - - if response.status == 404: - return False - - return True - - -def workflow_files(input: str) -> list: - """ - Takes in an argument of directory and/or files in string format from the CLI. - Returns a sorted set of all workflow files in the path(s) specified. - """ - workflow_files = [] - for path in input.split(): - if os.path.isfile(path): - workflow_files.append(path) - elif os.path.isdir(path): - for subdir, dirs, files in os.walk(path): - for filename in files: - filepath = subdir + os.sep + filename - if filepath.endswith((".yml", ".yaml")): - workflow_files.append(filepath) - - return sorted(set(workflow_files)) - - -def get_action_update(action_id): - """ - Takes in an action id (bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945) - and checks the action repo for the newest version. - If there is a new version, return the url to the updated version. - """ - if "./" in action_id: - # Handle local workflow calls, return None since there will be no updates. - return None - - path, *hash = action_id.split("@") - - if path in memoized_action_update_urls: - return memoized_action_update_urls[path] - else: - if "bitwarden" in path: - path_list = path.split("/", 2) - url = f"https://api.github.com/repos/{path_list[0]}/{path_list[1]}/commits?path={path_list[2]}" - response = get_github_api_response(url, action_id) - if not response: - return None - - sha = json.loads(response.data)[0]["sha"] - if sha not in hash: - update_url = ( - f"https://github.com/{path_list[0]}/{path_list[1]}/commit/{sha}" - ) - memoized_action_update_urls[path] = update_url - return update_url - else: - # Get tag from latest release - response = get_github_api_response( - f"https://api.github.com/repos/{path}/releases/latest", action_id - ) - if not response: - return None - - tag_name = json.loads(response.data)["tag_name"] - - # Get the URL to the commit for the tag - response = get_github_api_response( - f"https://api.github.com/repos/{path}/git/ref/tags/{tag_name}", - action_id, - ) - if not response: - return None - - if json.loads(response.data)["object"]["type"] == "commit": - sha = json.loads(response.data)["object"]["sha"] - else: - url = json.loads(response.data)["object"]["url"] - # Follow the URL and get the commit sha for tags - response = get_github_api_response(url, action_id) - if not response: - return None - - sha = json.loads(response.data)["object"]["sha"] - - if sha not in hash: - update_url = f"https://github.com/{path}/commit/{sha}" - memoized_action_update_urls[path] = update_url - return update_url - - -def lint(filename): - findings = [] - max_error_level = 0 - - with open(filename) as file: - workflow = yaml.load(file, Loader=yaml.FullLoader) - - # Check for 'name' key for the workflow. - if "name" not in workflow: - findings.append(LintFinding("Name key missing for workflow.", "warning")) - - # Check for 'name' value to be capitalized in workflow. - elif not workflow["name"][0].isupper(): - findings.append( - LintFinding( - f"Name value for workflow is not capitalized. [{workflow['name']}]", - "warning", - ) - ) - - # Loop through jobs in workflow. - if "jobs" in workflow: - jobs = workflow["jobs"] - for job_key in jobs: - job = jobs[job_key] - - # Make sure runner is using pinned version. - runner = job.get("runs-on", "") - if "-latest" in runner: - findings.append( - LintFinding( - f"Runner version is set to '{runner}', but needs to be pinned to a version.", - "warning", - ) - ) - - # Check for 'name' key for job. - if "name" not in job: - findings.append( - LintFinding( - f"Name key missing for job key '{job_key}'.", "warning" - ) - ) - # Check for 'name' value to be capitalized in job. - elif not job["name"][0].isupper(): - findings.append( - LintFinding( - f"Name value of job key '{job_key}' is not capitalized. [{job['name']}]", - "warning", - ) - ) - - # If the job has environment variables defined, then make sure they start with an underscore. - if "env" in job: - for k in job["env"].keys(): - if k[0] != "_": - findings.append( - LintFinding( - f"Environment variable '{k}' of job key '{job_key}' does not start with an underscore.", - "warning", - ) - ) - - # Loop through steps in job. - steps = job.get("steps", "") - for i, step in enumerate(steps, start=1): - # Check for 'name' key for step. - if "name" not in step: - findings.append( - LintFinding( - f"Name key missing for step {str(i)} of job key '{job_key}'.", - "warning", - ) - ) - # Check for 'name' value to be capitalized in step. - elif not step["name"][0].isupper(): - findings.append( - LintFinding( - f"Name value in step {str(i)} of job key '{job_key}' is not capitalized. [{step['name']}]", - "warning", - ) - ) - - if "uses" in step: - try: - path, hash = step["uses"].split("@") - except ValueError: - logging.info("Skipping local action in workflow.") - break - - # If the step has a 'uses' key, check value hash. - try: - # Check to make sure SHA1 hash is 40 characters. - if len(hash) != 40: - findings.append( - LintFinding( - f"Step {str(i)} of job key '{job_key}' does not have a valid action hash. (not 40 characters)", - "error", - ) - ) - - # Attempts to convert the hash to a integer - # which will succeed if all characters are hexadecimal - try: - int(hash, 16) - except ValueError: - findings.append( - LintFinding( - f"Step {str(i)} of job key '{job_key}' does not have a valid action hash. (not all hexadecimal characters)", - "error", - ) - ) - except: - findings.append( - LintFinding( - f"Step {str(i)} of job key '{job_key}' does not have a valid action hash. (missing '@' character)", - "error", - ) - ) - - # If the step has a 'uses' key, check path for external workflow - path_list = path.split("/", 2) - - if "bitwarden" in path and len(path_list) < 3: - findings.append( - LintFinding( - f"Step {str(i)} of job key '{job_key}' does not have a valid action path. (missing name of the repository or workflow)", - "error", - ) - ) - elif len(path_list) < 2: - findings.append( - LintFinding( - f"Step {str(i)} of job key '{job_key}' does not have a valid action path. (missing workflow name or the workflow author)", - "error", - ) - ) - # Check if GitHub repository with action exists - elif not action_repo_exists(step["uses"]): - action_id = step["uses"] - findings.append( - LintFinding( - f"Step {str(i)} of job key '{job_key}' uses an non-existing action: {action_id}.", - "error", - ) - ) - else: - # If the step has a 'uses' key and path is correct, check the action id repo for an update. - update_available = get_action_update(step["uses"]) - if update_available: - findings.append( - LintFinding( - f"Step {str(i)} of job key '{job_key}' uses an outdated action, consider updating it '{update_available}'.", - "warning", - ) - ) - - # If the step has a 'run' key and only has one command, check if it's a single line. - if "run" in step: - if step["run"].count("\n") == 1: - findings.append( - LintFinding( - f"Run in step {str(i)} of job key '{job_key}' should be a single line.", - "error", - ) - ) - - if len(findings) > 0: - print("#", filename) - for finding in findings: - print_finding(finding) - print() - - max_error_level = get_max_error_level(findings) - - return max_error_level - - -def main(input_args=None): - # Pull the arguments from the command line - if not input_args: - input_args = sys.argv[1:] - - # Read arguments from command line. - parser = argparse.ArgumentParser() - parser.add_argument("input", help="file or directory input") - parser.add_argument( - "-s", - "--strict", - action="store_true", - help="return non-zero exit code on warnings " "as well as errors", - ) - args = parser.parse_args(input_args) - # max_error_level = 0 - - # for filename in input_files: - # prob_level = lint(filename) - # max_error_level = max(max_error_level, prob_level) - input_files = workflow_files(args.input) - if len(input_files) > 0: - prob_levels = list(map(lint, input_files)) - - max_error_level = max(prob_levels) - - if max_error_level == PROBLEM_LEVELS["error"]: - return_code = 2 - elif max_error_level == PROBLEM_LEVELS["warning"]: - return_code = 1 if args.strict else 0 - else: - return_code = 0 - - return return_code - else: - print(f'File(s)/Directory: "{args.input}" does not exist, exiting.') - return -1 - - -if __name__ == "__main__": - return_code = main() - print(memoized_action_update_urls) - sys.exit(return_code) From 0232bd5e07b68dfc454db74aeaf82db1c4571652 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Thu, 11 Jan 2024 07:19:29 -0800 Subject: [PATCH 19/57] First linting error fixes --- lint-workflow/cli.py | 21 ++++++++++----------- lint-workflow/settings.py | 5 +++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lint-workflow/cli.py b/lint-workflow/cli.py index 0802f541..ca2a6eac 100644 --- a/lint-workflow/cli.py +++ b/lint-workflow/cli.py @@ -1,8 +1,7 @@ -import sys +"""This is the entrypoint module for the workflow-linter CLI.""" + import argparse -import os -import yaml -import json +import sys # from src.rules import workflow_rules, job_rules, step_rules, uses_step_rules, run_step_rules import settings @@ -15,16 +14,16 @@ local_settings = Settings( enabled_rules=settings.enabled_rules, approved_actions=settings.approved_actions ) -except: +except Exception as exc: raise SettingsError( ( "Required settings: enabled_rules, approved_actions\n" "Please see documentation for more information" ) - ) + ) from exc -def main(input_args=None): +def main(input_args: list[str] = None) -> int: """CLI utility to lint GitHub Action Workflows. A CLI utility to enforce coding standards on GitHub Action workflows. The @@ -55,11 +54,11 @@ def main(input_args=None): if args.command == "actions": if args.actions_command == "add": return actions_cmd.add(args.name, args.output) - elif args.actions_command == "update": + if args.actions_command == "update": return actions_cmd.update(args.output) - return -1 + + return -1 if __name__ == "__main__": - return_code = main() - sys.exit(return_code) + sys.exit(main()) diff --git a/lint-workflow/settings.py b/lint-workflow/settings.py index c86357e1..2b8931f5 100644 --- a/lint-workflow/settings.py +++ b/lint-workflow/settings.py @@ -1,5 +1,6 @@ +"""This psuedo-module is essentially configuration-as-code.""" + import json -import os enabled_rules = [ @@ -11,5 +12,5 @@ ] -with open("actions.json", "r") as action_file: +with open("actions.json", "r", encoding="utf8") as action_file: approved_actions = json.loads(action_file.read()) From 3a233a5f88001df14118022448e87bcb5b0289b4 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Thu, 11 Jan 2024 15:33:08 -0800 Subject: [PATCH 20/57] Fix all non-rule linter issues --- lint-workflow/Pipfile | 1 + lint-workflow/Pipfile.lock | 292 +++++++++++++++++++++- lint-workflow/README.md | 12 +- lint-workflow/Taskfile.yml | 10 + lint-workflow/cli.py | 2 - lint-workflow/pylintrc | 401 ++++++++++++++++++++++++++++++ lint-workflow/settings.py | 1 - lint-workflow/src/actions.py | 45 ++-- lint-workflow/src/lint.py | 41 +-- lint-workflow/src/load.py | 35 ++- lint-workflow/src/rule.py | 10 +- lint-workflow/src/utils.py | 19 +- lint-workflow/tests/test_load.py | 22 +- lint-workflow/tests/test_rule.py | 42 ++-- lint-workflow/tests/test_utils.py | 18 +- 15 files changed, 844 insertions(+), 107 deletions(-) create mode 100644 lint-workflow/pylintrc diff --git a/lint-workflow/Pipfile b/lint-workflow/Pipfile index 6918fd79..8f75f162 100644 --- a/lint-workflow/Pipfile +++ b/lint-workflow/Pipfile @@ -16,6 +16,7 @@ pytest = "*" coverage = "*" pytest-cov = "*" pylint = "*" +pytype = "*" [requires] python_version = "3.11" diff --git a/lint-workflow/Pipfile.lock b/lint-workflow/Pipfile.lock index 5822b002..d891658d 100644 --- a/lint-workflow/Pipfile.lock +++ b/lint-workflow/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "c238af6c0c1de4a035edbf90e66bdbd46d0fd8ce05f368d1c89dd668427f6755" + "sha256": "8b34ae1118eb5c9fac2c552c644f66f9dbd05415e08adf78efb99ba8a7f072f3" }, "pipfile-spec": 6, "requires": { @@ -34,11 +34,11 @@ }, "marshmallow": { "hashes": [ - "sha256:5d2371bbe42000f2b3fb5eaa065224df7d8f8597bc19a1bbfa5bfe7fba8da889", - "sha256:684939db93e80ad3561392f47be0230743131560a41c5110684c16e21ade0a5c" + "sha256:4c1daff273513dc5eb24b219a8035559dc573c8f322558ef85f5438ddd1236dd", + "sha256:c21d4b98fee747c130e6bc8f45c4b3199ea66bc00c12ee1f639f0aeca034d5e9" ], "markers": "python_version >= '3.8'", - "version": "==3.20.1" + "version": "==3.20.2" }, "mypy-extensions": { "hashes": [ @@ -328,6 +328,14 @@ "markers": "python_full_version >= '3.8.0'", "version": "==3.0.2" }, + "attrs": { + "hashes": [ + "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", + "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2.0" + }, "black": { "hashes": [ "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50", @@ -430,6 +438,14 @@ "markers": "python_version >= '3.11'", "version": "==0.3.7" }, + "importlab": { + "hashes": [ + "sha256:124cfa00e8a34fefe8aac1a5e94f56c781b178c9eb61a1d3f60f7e03b77338d3", + "sha256:b3893853b1f6eb027da509c3b40e6787e95dd66b4b66f1b3613aad77556e1465" + ], + "markers": "python_full_version >= '3.6.0'", + "version": "==0.8.1" + }, "iniconfig": { "hashes": [ "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", @@ -446,6 +462,117 @@ "markers": "python_full_version >= '3.8.0'", "version": "==5.13.2" }, + "jinja2": { + "hashes": [ + "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa", + "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.3" + }, + "libcst": { + "hashes": [ + "sha256:003e5e83a12eed23542c4ea20fdc8de830887cc03662432bb36f84f8c4841b81", + "sha256:0acbacb9a170455701845b7e940e2d7b9519db35a86768d86330a0b0deae1086", + "sha256:0bf69cbbab5016d938aac4d3ae70ba9ccb3f90363c588b3b97be434e6ba95403", + "sha256:2d37326bd6f379c64190a28947a586b949de3a76be00176b0732c8ee87d67ebe", + "sha256:3a07ecfabbbb8b93209f952a365549e65e658831e9231649f4f4e4263cad24b1", + "sha256:3ebbb9732ae3cc4ae7a0e97890bed0a57c11d6df28790c2b9c869f7da653c7c7", + "sha256:4bc745d0c06420fe2644c28d6ddccea9474fb68a2135904043676deb4fa1e6bc", + "sha256:5297a16e575be8173185e936b7765c89a3ca69d4ae217a4af161814a0f9745a7", + "sha256:5f1cd308a4c2f71d5e4eec6ee693819933a03b78edb2e4cc5e3ad1afd5fb3f07", + "sha256:63f75656fd733dc20354c46253fde3cf155613e37643c3eaf6f8818e95b7a3d1", + "sha256:73c086705ed34dbad16c62c9adca4249a556c1b022993d511da70ea85feaf669", + "sha256:75816647736f7e09c6120bdbf408456f99b248d6272277eed9a58cf50fb8bc7d", + "sha256:78b7a38ec4c1c009ac39027d51558b52851fb9234669ba5ba62283185963a31c", + "sha256:7ccaf53925f81118aeaadb068a911fac8abaff608817d7343da280616a5ca9c1", + "sha256:82d1271403509b0a4ee6ff7917c2d33b5a015f44d1e208abb1da06ba93b2a378", + "sha256:8ae11eb1ea55a16dc0cdc61b41b29ac347da70fec14cc4381248e141ee2fbe6c", + "sha256:8afb6101b8b3c86c5f9cec6b90ab4da16c3c236fe7396f88e8b93542bb341f7c", + "sha256:8c1f2da45f1c45634090fd8672c15e0159fdc46853336686959b2d093b6e10fa", + "sha256:97fbc73c87e9040e148881041fd5ffa2a6ebf11f64b4ccb5b52e574b95df1a15", + "sha256:99fdc1929703fd9e7408aed2e03f58701c5280b05c8911753a8d8619f7dfdda5", + "sha256:9dffa1795c2804d183efb01c0f1efd20a7831db6a21a0311edf90b4100d67436", + "sha256:bca1841693941fdd18371824bb19a9702d5784cd347cb8231317dbdc7062c5bc", + "sha256:c653d9121d6572d8b7f8abf20f88b0a41aab77ff5a6a36e5a0ec0f19af0072e8", + "sha256:c8f26250f87ca849a7303ed7a4fd6b2c7ac4dec16b7d7e68ca6a476d7c9bfcdb", + "sha256:cc9b6ac36d7ec9db2f053014ea488086ca2ed9c322be104fbe2c71ca759da4bb", + "sha256:d22d1abfe49aa60fc61fa867e10875a9b3024ba5a801112f4d7ba42d8d53242e", + "sha256:d68c34e3038d3d1d6324eb47744cbf13f2c65e1214cf49db6ff2a6603c1cd838", + "sha256:e3d8cf974cfa2487b28f23f56c4bff90d550ef16505e58b0dca0493d5293784b", + "sha256:f36f592e035ef84f312a12b75989dde6a5f6767fe99146cdae6a9ee9aff40dd0", + "sha256:f561c9a84eca18be92f4ad90aa9bd873111efbea995449301719a1a7805dbc5c", + "sha256:fe41b33aa73635b1651f64633f429f7aa21f86d2db5748659a99d9b7b1ed2a90" + ], + "markers": "python_version >= '3.8'", + "version": "==1.1.0" + }, + "markupsafe": { + "hashes": [ + "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e", + "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e", + "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431", + "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686", + "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c", + "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559", + "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc", + "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb", + "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939", + "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c", + "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0", + "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4", + "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9", + "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575", + "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba", + "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d", + "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd", + "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3", + "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00", + "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155", + "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac", + "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52", + "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f", + "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8", + "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b", + "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007", + "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24", + "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea", + "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198", + "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0", + "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee", + "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be", + "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2", + "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1", + "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707", + "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6", + "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c", + "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58", + "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823", + "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779", + "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636", + "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c", + "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad", + "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee", + "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc", + "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2", + "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48", + "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7", + "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e", + "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b", + "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa", + "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5", + "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e", + "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb", + "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9", + "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57", + "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc", + "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc", + "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2", + "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.3" + }, "mccabe": { "hashes": [ "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", @@ -462,6 +589,34 @@ "markers": "python_version >= '3.5'", "version": "==1.0.0" }, + "networkx": { + "hashes": [ + "sha256:4f33f68cb2afcf86f28a45f43efc27a9386b535d567d2127f8f61d51dec58d36", + "sha256:de346335408f84de0eada6ff9fafafff9bcda11f0a0dfaa931133debb146ab61" + ], + "markers": "python_version >= '3.8'", + "version": "==3.1" + }, + "ninja": { + "hashes": [ + "sha256:18302d96a5467ea98b68e1cae1ae4b4fb2b2a56a82b955193c637557c7273dbd", + "sha256:185e0641bde601e53841525c4196278e9aaf4463758da6dd1e752c0a0f54136a", + "sha256:376889c76d87b95b5719fdd61dd7db193aa7fd4432e5d52d2e44e4c497bdbbee", + "sha256:3e0f9be5bb20d74d58c66cc1c414c3e6aeb45c35b0d0e41e8d739c2c0d57784f", + "sha256:73b93c14046447c7c5cc892433d4fae65d6364bec6685411cb97a8bcf815f93a", + "sha256:7563ce1d9fe6ed5af0b8dd9ab4a214bf4ff1f2f6fd6dc29f480981f0f8b8b249", + "sha256:76482ba746a2618eecf89d5253c0d1e4f1da1270d41e9f54dfbd91831b0f6885", + "sha256:84502ec98f02a037a169c4b0d5d86075eaf6afc55e1879003d6cab51ced2ea4b", + "sha256:95da904130bfa02ea74ff9c0116b4ad266174fafb1c707aa50212bc7859aebf1", + "sha256:9d793b08dd857e38d0b6ffe9e6b7145d7c485a42dcfea04905ca0cdb6017cc3c", + "sha256:9df724344202b83018abb45cb1efc22efd337a1496514e7e6b3b59655be85205", + "sha256:aad34a70ef15b12519946c5633344bc775a7656d789d9ed5fdb0d456383716ef", + "sha256:d491fc8d89cdcb416107c349ad1e3a735d4c4af5e1cb8f5f727baca6350fdaea", + "sha256:ecf80cf5afd09f14dcceff28cb3f11dc90fb97c999c89307aea435889cb66877", + "sha256:fa2ba9d74acfdfbfbcf06fad1b8282de8a7a8c481d9dee45c859a8c93fcc1082" + ], + "version": "==1.11.1.1" + }, "packaging": { "hashes": [ "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", @@ -494,6 +649,22 @@ "markers": "python_version >= '3.8'", "version": "==1.3.0" }, + "pycnite": { + "hashes": [ + "sha256:7d02eb0ec4b405d8812ce053434dacfc2335dcd458ab58a1a8bf64f72d40bd76", + "sha256:ad8616982beecc39f2090999aa8fe0b044b1f6733ec39484cb5e0900b3c88aa1" + ], + "markers": "python_version >= '3.8'", + "version": "==2023.10.11" + }, + "pydot": { + "hashes": [ + "sha256:408a47913ea7bd5d2d34b274144880c1310c4aee901f353cf21fe2e526a4ea28", + "sha256:60246af215123fa062f21cd791be67dda23a6f280df09f68919e637a1e4f3235" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, "pylint": { "hashes": [ "sha256:58c2398b0301e049609a8429789ec6edf3aabe9b6c5fec916acd18639c16de8b", @@ -502,6 +673,14 @@ "index": "pypi", "version": "==3.0.3" }, + "pyparsing": { + "hashes": [ + "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb", + "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db" + ], + "markers": "python_full_version >= '3.6.8'", + "version": "==3.1.1" + }, "pytest": { "hashes": [ "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", @@ -518,6 +697,96 @@ "index": "pypi", "version": "==4.1.0" }, + "pytype": { + "hashes": [ + "sha256:0e59fb2866cdd804b31ef9baa0f53bc6fddaeaf68a7b1af6aa62c1b9854f017e", + "sha256:144e83bb7b80e5b0972a25bed2e4f03c0cd91d677ad0902bb775fdac156151ca", + "sha256:20680085e5a7beee2aaedddbf96863efa96ff047061ae82e31404336180502f5", + "sha256:33227cd847df1c5e92dbe013dc3926b78c77b36032a1caa3e488286aa9a0a14e", + "sha256:5281cc89ba5acc5a9184845f5c02319c0fbfcd87a9ab4920b6bce0d0caec6860", + "sha256:734d34b3ce13ccea64c419f88630fd189d6d06cbb24516cfb6fe36edc1860da2", + "sha256:a964a105af46fff3495be76ee34ad34acf47300018244122ea75d0e777d04cb5", + "sha256:af4b7ced6049e7fececb646262a25874e8a4e6a0b7e540c100b673aacd230ffa", + "sha256:b1b802867ede7cfd7dbe479cfdd3a1341dac005bfcb2718ff22070494fdd57be", + "sha256:b1c5857acd6348e9f5ace6427d74824f433d6bbfad0e0104b98f5078f5781af6", + "sha256:bf28233b140e9a7702cffcc8346c8d47b492813bee5f11ba2cef906ff4c05c55", + "sha256:c22db76f45a218c673f70c4ab32e7de757b808ce5d9ae55d1b3621f05187c496" + ], + "index": "pypi", + "version": "==2024.1.5" + }, + "pyyaml": { + "hashes": [ + "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", + "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", + "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", + "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", + "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", + "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", + "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", + "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", + "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", + "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", + "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", + "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", + "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", + "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", + "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", + "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", + "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", + "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", + "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", + "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", + "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", + "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", + "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", + "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", + "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", + "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", + "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", + "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", + "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", + "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", + "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", + "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", + "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", + "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", + "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", + "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", + "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", + "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", + "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", + "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", + "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", + "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", + "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", + "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", + "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", + "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", + "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", + "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", + "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", + "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" + ], + "index": "pypi", + "version": "==6.0.1" + }, + "tabulate": { + "hashes": [ + "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", + "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f" + ], + "markers": "python_version >= '3.7'", + "version": "==0.9.0" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.10.2" + }, "tomlkit": { "hashes": [ "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4", @@ -525,6 +794,21 @@ ], "markers": "python_version >= '3.7'", "version": "==0.12.3" + }, + "typing-extensions": { + "hashes": [ + "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", + "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + ], + "markers": "python_version >= '3.8'", + "version": "==4.9.0" + }, + "typing-inspect": { + "hashes": [ + "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", + "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78" + ], + "version": "==0.9.0" } } } diff --git a/lint-workflow/README.md b/lint-workflow/README.md index 782adacb..f4d65a0d 100644 --- a/lint-workflow/README.md +++ b/lint-workflow/README.md @@ -49,7 +49,7 @@ pytest tests --cov=src ### Code Reformatting -We strictly adhere to PEP8 and use `black` to maintain this adherence. `black` should be run on any change being merged +We adhere to PEP8 and use `black` to maintain this adherence. `black` should be run on any change being merged to `main`. ``` @@ -57,6 +57,16 @@ pipenv shell black . ``` +### Linting + +We loosely use [Google's Python style guide](https://google.github.io/styleguide/pyguide.html), but yield to +`black` when there is a conflict + +``` +pipenv shell +pylint --rcfile pylintrc src/ tests/ +``` + ### Add a new Rule A new Rule is created by extending the Rule base class and overriding the `fn(obj: Union[Workflow, Job, Step])` method. diff --git a/lint-workflow/Taskfile.yml b/lint-workflow/Taskfile.yml index 83c9b785..673a01a1 100644 --- a/lint-workflow/Taskfile.yml +++ b/lint-workflow/Taskfile.yml @@ -7,6 +7,16 @@ tasks: silent: true cmds: - pipenv run black . + + lint: + silent: true + cmds: + - pipenv run pylint --rcflie pylintrc {{.CLI_ARGS}} + + type: + silent: true + cmds: + - pipenv run pytype src test:unit: cmds: diff --git a/lint-workflow/cli.py b/lint-workflow/cli.py index ca2a6eac..4da10380 100644 --- a/lint-workflow/cli.py +++ b/lint-workflow/cli.py @@ -1,9 +1,7 @@ """This is the entrypoint module for the workflow-linter CLI.""" - import argparse import sys -# from src.rules import workflow_rules, job_rules, step_rules, uses_step_rules, run_step_rules import settings from src.actions import ActionsCmd from src.utils import Settings, SettingsError diff --git a/lint-workflow/pylintrc b/lint-workflow/pylintrc new file mode 100644 index 00000000..e2378102 --- /dev/null +++ b/lint-workflow/pylintrc @@ -0,0 +1,401 @@ +# This Pylint rcfile contains a best-effort configuration to uphold the +# best-practices and style described in the Google Python style guide: +# https://google.github.io/styleguide/pyguide.html +# +# Its canonical open-source location is: +# https://google.github.io/styleguide/pylintrc + +[MAIN] + +# Files or directories to be skipped. They should be base names, not paths. +ignore=third_party + +# Files or directories matching the regex patterns are skipped. The regex +# matches against base names, not paths. +ignore-patterns= + +# Pickle collected data for later comparisons. +persistent=no + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Use multiple processes to speed up Pylint. +jobs=4 + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=R, + abstract-method, + apply-builtin, + arguments-differ, + attribute-defined-outside-init, + backtick, + bad-option-value, + basestring-builtin, + buffer-builtin, + c-extension-no-member, + consider-using-enumerate, + cmp-builtin, + cmp-method, + coerce-builtin, + coerce-method, + delslice-method, + div-method, + eq-without-hash, + execfile-builtin, + file-builtin, + filter-builtin-not-iterating, + fixme, + getslice-method, + global-statement, + hex-method, + idiv-method, + implicit-str-concat, + import-error, + import-self, + import-star-module-level, + input-builtin, + intern-builtin, + invalid-str-codec, + locally-disabled, + long-builtin, + long-suffix, + map-builtin-not-iterating, + misplaced-comparison-constant, + missing-function-docstring, + metaclass-assignment, + next-method-called, + next-method-defined, + no-absolute-import, + no-init, # added + no-member, + no-name-in-module, + no-self-use, + nonzero-method, + oct-method, + old-division, + old-ne-operator, + old-octal-literal, + old-raise-syntax, + parameter-unpacking, + print-statement, + raising-string, + range-builtin-not-iterating, + raw_input-builtin, + rdiv-method, + reduce-builtin, + relative-import, + reload-builtin, + round-builtin, + setslice-method, + signature-differs, + standarderror-builtin, + suppressed-message, + sys-max-int, + trailing-newlines, + unichr-builtin, + unicode-builtin, + unnecessary-pass, + unpacking-in-except, + useless-else-on-loop, + useless-suppression, + using-cmp-argument, + wrong-import-order, + xrange-builtin, + zip-builtin-not-iterating, + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages +reports=no + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[BASIC] + +# Good variable names which should always be accepted, separated by a comma +good-names=main,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty,cached_property.cached_property,cached_property.threaded_cached_property,cached_property.cached_property_with_ttl,cached_property.threaded_cached_property_with_ttl + +# Regular expression matching correct function names +function-rgx=^(?:(?PsetUp|tearDown|setUpModule|tearDownModule)|(?P_?[A-Z][a-zA-Z0-9]*)|(?P_?[a-z][a-z0-9_]*))$ + +# Regular expression matching correct variable names +variable-rgx=^[a-z][a-z0-9_]*$ + +# Regular expression matching correct constant names +const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ + +# Regular expression matching correct attribute names +attr-rgx=^_{0,2}[a-z][a-z0-9_]*$ + +# Regular expression matching correct argument names +argument-rgx=^[a-z][a-z0-9_]*$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=^[a-z][a-z0-9_]*$ + +# Regular expression matching correct class names +class-rgx=^_?[A-Z][a-zA-Z0-9]*$ + +# Regular expression matching correct module names +module-rgx=^(_?[a-z][a-z0-9_]*|__init__)$ + +# Regular expression matching correct method names +method-rgx=(?x)^(?:(?P_[a-z0-9_]+__|runTest|setUp|tearDown|setUpTestCase|tearDownTestCase|setupSelf|tearDownClass|setUpClass|(test|assert)_*[A-Z0-9][a-zA-Z0-9_]*|next)|(?P_{0,2}[A-Z][a-zA-Z0-9_]*)|(?P_{0,2}[a-z][a-z0-9_]*))$ + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=(__.*__|main|test.*|.*test|.*Test)$ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=12 + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=88 + +# TODO(https://github.com/pylint-dev/pylint/issues/3352): Direct pylint to exempt +# lines made too long by directives to pytype. + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=(?x)( + ^\s*(\#\ )??$| + ^\s*(from\s+\S+\s+)?import\s+.+$) + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=yes + +# Maximum number of lines in a module +max-module-lines=99999 + +# String used as indentation unit. The internal Google style guide mandates 2 +# spaces. Google's externaly-published style guide says 4, consistent with +# PEP 8. Here, we use 2 spaces, for conformity with many open-sourced Google +# projects (like TensorFlow). + +# Overriden to 4 to conform with black +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=TODO + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=yes + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_) + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six,six.moves,past.builtins,future.builtins,functools + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging,absl.logging,tensorflow.io.logging + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub, + TERMIOS, + Bastion, + rexec, + sets + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant, absl + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls, + class_ + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs diff --git a/lint-workflow/settings.py b/lint-workflow/settings.py index 2b8931f5..a8c72cc5 100644 --- a/lint-workflow/settings.py +++ b/lint-workflow/settings.py @@ -1,5 +1,4 @@ """This psuedo-module is essentially configuration-as-code.""" - import json diff --git a/lint-workflow/src/actions.py b/lint-workflow/src/actions.py index 6de78cf1..eff1bba0 100644 --- a/lint-workflow/src/actions.py +++ b/lint-workflow/src/actions.py @@ -1,3 +1,4 @@ +"""Module providing Actions subcommand to manage list of pre-approved Actions.""" import argparse import json import logging @@ -7,20 +8,24 @@ from dataclasses import asdict from typing import Union, Tuple -from src.utils import Colors, Settings, SettingsError, Action +from src.utils import Colors, Settings, Action class ActionsCmd: - def __init__(self, settings: Settings = None) -> None: - """Command to manage the pre-approved list of Actions + """Command to manage the pre-approved list of Actions + + This class contains logic to manage the list of pre-approved actions + to include: + - updating the action data in the list + - adding a new pre-approved action to the list with the data from the + latest release - This class contains logic to manage the list of pre-approved actions - to include: - - updating the action data in the list - - adding a new pre-approved action to the list with the data from the - latest release + This class also includes supporting logic to interact with GitHub - This class also includes supporting logic to interact with GitHub + """ + + def __init__(self, settings: Settings = None) -> None: + """Initialize the the ActionsCmd class. Args: settings: @@ -41,8 +46,7 @@ def extend_parser(subparsers: argparse.ArgumentParser) -> argparse.ArgumentParse The main argument parser to add sub commands and arguments to """ parser_actions = subparsers.add_parser( - "actions", - help="Add or Update Actions in the pre-approved list." + "actions", help="Add or Update Actions in the pre-approved list." ) parser_actions.add_argument( "-o", "--output", action="store", default="actions.json" @@ -50,7 +54,7 @@ def extend_parser(subparsers: argparse.ArgumentParser) -> argparse.ArgumentParse subparsers_actions = parser_actions.add_subparsers( required=True, dest="actions_command" ) - parser_actions_update = subparsers_actions.add_parser( + subparsers_actions.add_parser( "update", help="update action versions" ) parser_actions_add = subparsers_actions.add_parser( @@ -75,13 +79,16 @@ def get_github_api_response( if response.status == 403 and response.reason == "rate limit exceeded": logging.error( - f"Failed to call GitHub API for action: {action_name} due to rate limit exceeded." + "Failed to call GitHub API for action: %s due to rate limit exceeded.", + action_name ) return None if response.status == 401 and response.reason == "Unauthorized": logging.error( - f"Failed to call GitHub API for action: {action_name}: {response.data}." + "Failed to call GitHub API for action: %s: %s.", + action_name, + response.data ) return None @@ -94,8 +101,9 @@ def exists(self, action: Action) -> bool: response = self.get_github_api_response(url, action.name) if response is None: - # Handle github api limit exceed by returning that the action exists without actually checking - # to prevent false errors on linter output. Only show it as an linter error. + # Handle github api limit exceed by returning that the action exists + # without actually checking to prevent false errors on linter output. Only + # show it as an linter error. return True if response.status == 404: @@ -141,7 +149,7 @@ def save_actions(self, updated_actions: dict[str, Action], filename: str) -> Non This is used to track the list of approved actions. """ - with open(filename, "w") as action_file: + with open(filename, "w", encoding="utf8") as action_file: converted_updated_actions = { name: asdict(action) for name, action in updated_actions.items() } @@ -182,7 +190,8 @@ def update(self, filename: str) -> None: print( ( f" - {action.name} \033[{Colors.yellow}changed\033[0m: " - f"({action.version}, {action.sha}) => ({latest_release.version}, {latest_release.sha})" + f"({action.version}, {action.sha}) => (" + f"{latest_release.version}, {latest_release.sha})" ) ) else: diff --git a/lint-workflow/src/lint.py b/lint-workflow/src/lint.py index 647ab28f..97145b7d 100644 --- a/lint-workflow/src/lint.py +++ b/lint-workflow/src/lint.py @@ -1,20 +1,24 @@ +"""Module providing Lint subcommand to run custom linting rules against GitHub Action +Workflows.""" import argparse import os from functools import reduce from src.load import WorkflowBuilder, Rules -from src.utils import Colors, LintFinding, Settings, SettingsError +from src.utils import LintFinding, Settings class LinterCmd: - def __init__(self, settings: Settings = None) -> None: - """Command to lint GitHub Action Workflow files + """Command to lint GitHub Action Workflow files - This class contains logic to lint workflows that are passed in. - Supporting logic is supplied to: - - build out the list of Rules desired - - select and validate the workflow files to lint + This class contains logic to lint workflows that are passed in. + Supporting logic is supplied to: + - build out the list of Rules desired + - select and validate the workflow files to lint + """ + def __init__(self, settings: Settings = None) -> None: + """Initailized the LinterCmd class. Args: settings: @@ -35,7 +39,7 @@ def extend_parser(subparsers: argparse.ArgumentParser) -> argparse.ArgumentParse """ parser_lint = subparsers.add_parser( "lint", - help="Verify that a GitHub Action Workflow follows all of the Rules." + help="Verify that a GitHub Action Workflow follows all of the Rules.", ) parser_lint.add_argument( "-s", @@ -86,19 +90,18 @@ def lint_file(self, filename: str) -> int: max_error_level = 0 print(f"Linting: {filename}") - with open(filename) as file: - workflow = WorkflowBuilder.build(filename) + workflow = WorkflowBuilder.build(filename) - for rule in self.rules.workflow: - findings.append(rule.execute(workflow)) + for rule in self.rules.workflow: + findings.append(rule.execute(workflow)) - for job_key, job in workflow.jobs.items(): - for rule in self.rules.job: - findings.append(rule.execute(job)) + for _, job in workflow.jobs.items(): + for rule in self.rules.job: + findings.append(rule.execute(job)) - for step in job.steps: - for rule in self.rules.step: - findings.append(rule.execute(step)) + for step in job.steps: + for rule in self.rules.step: + findings.append(rule.execute(step)) findings = list(filter(lambda a: a is not None, findings)) @@ -128,7 +131,7 @@ def generate_files(self, files: list[str]) -> list[str]: if os.path.isfile(path): workflow_files.append(path) elif os.path.isdir(path): - for subdir, dirs, files in os.walk(path): + for subdir, _, files in os.walk(path): for filename in files: filepath = subdir + os.sep + filename if filepath.endswith((".yml", ".yaml")): diff --git a/lint-workflow/src/load.py b/lint-workflow/src/load.py index 1dda9f71..2bdc902f 100644 --- a/lint-workflow/src/load.py +++ b/lint-workflow/src/load.py @@ -1,3 +1,4 @@ +"""Module to load for Worflows and Rules.""" import importlib from typing import List @@ -15,12 +16,14 @@ class WorkflowBuilderError(Exception): - """Custom Exception to indicate an error with the WorkflowBuilder.""" + """Exception to indicate an error with the WorkflowBuilder.""" pass class WorkflowBuilder: + """Collection of methods to build Workflow objects.""" + @classmethod def __load_workflow_from_file(cls, filename: str) -> CommentedMap: """Load YAML from disk. @@ -34,7 +37,7 @@ def __load_workflow_from_file(cls, filename: str) -> CommentedMap: yaml file. It includes the comments as a part of their respective objects (depending on their location in the file). """ - with open(filename) as file: + with open(filename, encoding="utf8") as file: return yaml.load(file) @classmethod @@ -69,7 +72,7 @@ def __build_workflow(cls, loaded_yaml: CommentedMap) -> Workflow: @classmethod def build( - cls, filename: str = None, yaml: CommentedMap = None, from_file: bool = True + cls, filename: str = None, workflow: CommentedMap = None, from_file: bool = True ) -> Workflow: """Build a Workflow from either code or a file. @@ -87,25 +90,33 @@ def build( """ if from_file and filename is not None: return cls.__build_workflow(cls.__load_workflow_from_file(filename)) - elif not from_file and yaml is not None: - return cls.__build_workflow(yaml) + elif not from_file and workflow is not None: + return cls.__build_workflow(workflow) - raise WorkflowBuilderException( + raise WorkflowBuilderError( "The workflow must either be built from a file or from a CommentedMap" ) +class LoadRulesError(Exception): + """Exception to indicate an error with loading rules.""" + + pass + + class Rules: + """A collection of all of the types of rules. + + Rules is used as a collection of which Rules apply to which parts of the + workflow. It also assists in making sure the Rules that apply to multiple + types are not skipped. + """ workflow: List[Rule] = [] job: List[Rule] = [] step: List[Rule] = [] def __init__(self, settings: Settings) -> None: - """A collection of all of the types of rules. - - Rules is used as a collection of which Rules apply to which parts of the - workflow. It also assists in making sure the Rules that apply to multiple - types are not skipped. + """Initializes the Rules Args: settings: @@ -127,7 +138,7 @@ def __init__(self, settings: Settings) -> None: self.job.append(rule_inst) if Step in rule_inst.compatibility: self.step.append(rule_inst) - except Exception as err: + except LoadRulesError as err: print(f"Error loading: {rule}\n{err}") def list(self) -> None: diff --git a/lint-workflow/src/rule.py b/lint-workflow/src/rule.py index 791c105b..23932544 100644 --- a/lint-workflow/src/rule.py +++ b/lint-workflow/src/rule.py @@ -1,3 +1,4 @@ +"""Base Rule class to build rules by extending.""" from typing import Union, List from .models.workflow import Workflow @@ -6,6 +7,11 @@ from .utils import LintFinding, LintLevels, Settings +class RuleExecutionException(Exception): + """Exeception for the Base Rule class.""" + pass + + class Rule: """Base class of a Rule to extend to create a linting Rule.""" @@ -24,7 +30,7 @@ def fn(self, obj: Union[Workflow, Job, Step]) -> bool: Returns: The success/failure of the result of the Rule ran on the input. """ - return False, self.message + return False, f"{obj.name}: {self.message}" def build_lint_message(self, message: str, obj: Union[Workflow, Job, Step]) -> str: """Build the lint failure message. @@ -83,7 +89,7 @@ def execute(self, obj: Union[Workflow, Job, Step]) -> Union[LintFinding, None]: if passed: return None - except Exception as err: + except RuleExecutionException as err: return LintFinding( self.build_lint_message( f"failed to apply {type(self).__name__}\n{err}", obj diff --git a/lint-workflow/src/utils.py b/lint-workflow/src/utils.py index 18e2b11a..1c762958 100644 --- a/lint-workflow/src/utils.py +++ b/lint-workflow/src/utils.py @@ -1,4 +1,5 @@ -from dataclasses import asdict, dataclass +"""Module of a collection of random utilities.""" +from dataclasses import dataclass from enum import Enum from typing import Self @@ -48,7 +49,10 @@ def __str__(self) -> str: Returns: String representation of itself. """ - return f"\033[{self.level.color}{self.level.name.lower()}\033[0m {self.description}" + return ( + f"\033[{self.level.color}{self.level.name.lower()}\033[0m " + f"{self.description}" + ) @dataclass @@ -95,13 +99,14 @@ class SettingsError(Exception): class Settings: + """Class that contains configuration-as-code for any portion of the app.""" enabled_rules: list[str] approved_actions: dict[str, Action] def __init__( self, - enabled_rules: list[str] = [], - approved_actions: dict[str, dict[str, str]] = {}, + enabled_rules: list[str] = None, + approved_actions: dict[str, dict[str, str]] = None, ): """Settings object that can be overriden in settings.py. @@ -113,6 +118,12 @@ def __init__( The colleciton of GitHub Actions that are pre-approved to be used in any workflow (Required by src.rules.step_approved) """ + if enabled_rules is None: + enabled_rules = [] + + if approved_actions is None: + approved_actions = {} + self.enabled_rules = enabled_rules self.approved_actions = { name: Action(**action) for name, action in approved_actions.items() diff --git a/lint-workflow/tests/test_load.py b/lint-workflow/tests/test_load.py index 38131563..1ec34222 100644 --- a/lint-workflow/tests/test_load.py +++ b/lint-workflow/tests/test_load.py @@ -1,25 +1,25 @@ -import json +"""Tests src/load.py.""" import pytest from ruamel.yaml import YAML from ruamel.yaml.comments import CommentedMap from .conftest import FIXTURE_DIR -from .context import src -from src.utils import Settings +from src.load import WorkflowBuilder +from src.models.workflow import Workflow yaml = YAML() -@pytest.fixture -def workflow_filename(): +@pytest.fixture(name="workflow_filename") +def fixture_workflow_filename(): return f"{FIXTURE_DIR}/test.yml" -@pytest.fixture -def workflow_yaml(): +@pytest.fixture(name="workflow_yaml") +def fixture_workflow_yaml(): return yaml.load( """\ --- @@ -39,10 +39,10 @@ def workflow_yaml(): def test_load_workflow_from_file(workflow_filename: str) -> None: - workflow = src.load.WorkflowBuilder.build(workflow_filename) - assert type(workflow) == src.models.Workflow + workflow = WorkflowBuilder.build(workflow_filename) + assert isinstance(workflow, Workflow) def test_load_workflow_from_yaml(workflow_yaml: CommentedMap) -> None: - workflow = src.load.WorkflowBuilder.build(yaml=workflow_yaml, from_file=False) - assert type(workflow) == src.models.Workflow + workflow = WorkflowBuilder.build(workflow=workflow_yaml, from_file=False) + assert isinstance(workflow, Workflow) diff --git a/lint-workflow/tests/test_rule.py b/lint-workflow/tests/test_rule.py index 4e43a8a1..4e0fa471 100644 --- a/lint-workflow/tests/test_rule.py +++ b/lint-workflow/tests/test_rule.py @@ -1,11 +1,9 @@ +"""Tests src/rule.py.""" import pytest from typing import Union from ruamel.yaml import YAML -from .conftest import FIXTURE_DIR -from .context import src - from src.load import WorkflowBuilder from src.rule import Rule from src.models import Workflow, Job, Step @@ -14,8 +12,8 @@ yaml = YAML() -@pytest.fixture -def correct_workflow(): +@pytest.fixture(name="correct_workflow") +def fixture_correct_workflow(): workflow = """\ --- name: Test Workflow @@ -31,11 +29,11 @@ def correct_workflow(): - name: Test uses: actions/checkout@main """ - return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) + return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) -@pytest.fixture -def incorrect_workflow(): +@pytest.fixture(name="incorrect_workflow") +def fixture_incorrect_workflow(): workflow = """\ --- on: @@ -47,7 +45,7 @@ def incorrect_workflow(): steps: - uses: actions/checkout@main """ - return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) + return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) class RuleStep(Rule): @@ -67,28 +65,32 @@ def fn(self, obj: Union[Workflow, Job, Step]) -> bool: return obj.name is not None, self.message +class TestException(Exception): + """Test Exception.""" + pass + + class RuleException(Rule): def __init__(self): self.message = "should raise Exception" self.on_fail = "error" def fn(self, obj: Union[Workflow, Job, Step]) -> bool: - raise Exception("test Exception") - return True, self.message + raise TestException("test Exception") -@pytest.fixture -def step_rule(): +@pytest.fixture(name="step_rule") +def fixture_step_rule(): return RuleStep() -@pytest.fixture -def exists_rule(): +@pytest.fixture(name="exists_rule") +def fixture_exists_rule(): return RuleNameExists() -@pytest.fixture -def exception_rule(): +@pytest.fixture(name="exception_rule") +def fixture_exception_rule(): return RuleException() @@ -119,9 +121,9 @@ def test_rule_compatibility(step_rule, correct_workflow): def test_correct_rule_execution(exists_rule, correct_workflow): - assert exists_rule.execute(correct_workflow) == None - assert exists_rule.execute(correct_workflow.jobs["job-key"]) == None - assert exists_rule.execute(correct_workflow.jobs["job-key"].steps[0]) == None + assert exists_rule.execute(correct_workflow) is None + assert exists_rule.execute(correct_workflow.jobs["job-key"]) is None + assert exists_rule.execute(correct_workflow.jobs["job-key"].steps[0]) is None def test_incorrect_rule_execution(exists_rule, incorrect_workflow): diff --git a/lint-workflow/tests/test_utils.py b/lint-workflow/tests/test_utils.py index d8756882..98aab502 100644 --- a/lint-workflow/tests/test_utils.py +++ b/lint-workflow/tests/test_utils.py @@ -1,12 +1,4 @@ -import json -import pytest - -from ruamel.yaml import YAML -from ruamel.yaml.comments import CommentedMap - -from .conftest import FIXTURE_DIR -from .context import src - +"""Tests src/utils.py.""" from src.utils import Action, Colors, LintFinding, LintLevels @@ -16,16 +8,16 @@ def test_action_eq(): action_a = Action(**action_def) action_b = Action(**action_def) - assert (action_a == action_b) == True - assert (action_a != action_b) == False + assert (action_a == action_b) is True + assert (action_a != action_b) is False def test_action_ne(): action_a = Action(name="bitwarden/sm-action", version="1.0.0", sha="some-sha") action_b = Action(name="bitwarden/sm-action", version="1.1.0", sha="some-other-sha") - assert (action_a == action_b) == False - assert (action_a != action_b) == True + assert (action_a == action_b) is False + assert (action_a != action_b) is True def test_lint_level(): From a2930d881b8a9176dbf458c896279e5141a55f92 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Thu, 11 Jan 2024 15:37:34 -0800 Subject: [PATCH 21/57] Switch to the cleaner json.load() function --- lint-workflow/Session.vim | 503 -------------------------------------- lint-workflow/settings.py | 2 +- 2 files changed, 1 insertion(+), 504 deletions(-) delete mode 100644 lint-workflow/Session.vim diff --git a/lint-workflow/Session.vim b/lint-workflow/Session.vim deleted file mode 100644 index 5a3a81a0..00000000 --- a/lint-workflow/Session.vim +++ /dev/null @@ -1,503 +0,0 @@ -let SessionLoad = 1 -let s:so_save = &g:so | let s:siso_save = &g:siso | setg so=0 siso=0 | setl so=-1 siso=-1 -let v:this_session=expand(":p") -silent only -silent tabonly -cd ~/projects/workflow-linter -if expand('%') == '' && !&modified && line('$') <= 1 && getline(1) == '' - let s:wipebuf = bufnr('%') -endif -let s:shortmess_save = &shortmess -if &shortmess =~ 'A' - set shortmess=aoOA -else - set shortmess=aoO -endif -badd +1 README.md -badd +24 cli.py -badd +4 src/load.py -badd +1 config.yaml -badd +1 src/rules/__init__.py -badd +19 src/rules/name_capitalized.py -badd +13 src/rules/name_exists.py -badd +1 src/rule.py -badd +72 tests/test_rule.py -badd +40 tests/rules/test_name_exists.py -badd +100 tests/rules/test_name_capitalized.py -badd +4 rule_settings.py -badd +12 settings.py -badd +3 tests/fixtures/test.yml -badd +1 tests/fixtures/test-min-incorrect.yaml -badd +10 tests/test_load.py -badd +2 src/rules/runs_on_pinned.py -badd +3 src/rules/pinned_workflow_runner.py -badd +3 tests/rules/test_pinned_workflow_runner.py -badd +15 src/models/workflow.py -badd +1 src/rules/pinned_job_runner.py -badd +9 tests/rules/test_pinned_job_runner.py -badd +19 src/rules/job_environment_prefix.py -badd +18 src/models/job.py -badd +72 tests/rules/test_job_environment_prefix.py -badd +19 src/rules/step_hex_length.py -badd +43 tests/rules/test_step_hex_length.py -badd +5 src/rules/step_hex.py -badd +9 tests/rules/test_step_hex.py -badd +47 src/rules/step_approved.py -badd +92 tests/rules/test_step_approved.py -badd +0 src/utils.py -badd +16 src/rules/step_pinned.py -badd +100 tests/rules/test_step_pinned.py -badd +1 tests/fixtures/test-min.yaml -badd +0 actions.json -badd +0 src/models/step.py -badd +0 tests/test_utils.py -badd +0 src/lint.py -badd +0 src/actions.py -badd +0 Taskfile.yml -argglobal -%argdel -$argadd README.md -set stal=2 -tabnew +setlocal\ bufhidden=wipe -tabnew +setlocal\ bufhidden=wipe -tabnew +setlocal\ bufhidden=wipe -tabnew +setlocal\ bufhidden=wipe -tabrewind -edit README.md -let s:save_splitbelow = &splitbelow -let s:save_splitright = &splitright -set splitbelow splitright -wincmd _ | wincmd | -vsplit -wincmd _ | wincmd | -vsplit -2wincmd h -wincmd w -wincmd w -wincmd _ | wincmd | -split -1wincmd k -wincmd w -let &splitbelow = s:save_splitbelow -let &splitright = s:save_splitright -wincmd t -let s:save_winminheight = &winminheight -let s:save_winminwidth = &winminwidth -set winminheight=0 -set winheight=1 -set winminwidth=0 -set winwidth=1 -exe 'vert 1resize ' . ((&columns * 109 + 163) / 327) -exe 'vert 2resize ' . ((&columns * 108 + 163) / 327) -exe '3resize ' . ((&lines * 32 + 33) / 67) -exe 'vert 3resize ' . ((&columns * 108 + 163) / 327) -exe '4resize ' . ((&lines * 31 + 33) / 67) -exe 'vert 4resize ' . ((&columns * 108 + 163) / 327) -argglobal -balt Taskfile.yml -setlocal fdm=manual -setlocal fde=0 -setlocal fmr={{{,}}} -setlocal fdi=# -setlocal fdl=0 -setlocal fml=1 -setlocal fdn=20 -setlocal fen -silent! normal! zE -let &fdl = &fdl -let s:l = 41 - ((36 * winheight(0) + 32) / 64) -if s:l < 1 | let s:l = 1 | endif -keepjumps exe s:l -normal! zt -keepjumps 41 -normal! 0 -wincmd w -argglobal -if bufexists(fnamemodify("Taskfile.yml", ":p")) | buffer Taskfile.yml | else | edit Taskfile.yml | endif -if &buftype ==# 'terminal' - silent file Taskfile.yml -endif -balt README.md -setlocal fdm=manual -setlocal fde=0 -setlocal fmr={{{,}}} -setlocal fdi=# -setlocal fdl=0 -setlocal fml=1 -setlocal fdn=20 -setlocal fen -silent! normal! zE -let &fdl = &fdl -let s:l = 29 - ((28 * winheight(0) + 32) / 64) -if s:l < 1 | let s:l = 1 | endif -keepjumps exe s:l -normal! zt -keepjumps 29 -normal! 021| -wincmd w -argglobal -if bufexists(fnamemodify("settings.py", ":p")) | buffer settings.py | else | edit settings.py | endif -if &buftype ==# 'terminal' - silent file settings.py -endif -balt actions.json -setlocal fdm=manual -setlocal fde=0 -setlocal fmr={{{,}}} -setlocal fdi=# -setlocal fdl=0 -setlocal fml=1 -setlocal fdn=20 -setlocal fen -silent! normal! zE -let &fdl = &fdl -let s:l = 5 - ((4 * winheight(0) + 16) / 32) -if s:l < 1 | let s:l = 1 | endif -keepjumps exe s:l -normal! zt -keepjumps 5 -normal! 0 -wincmd w -argglobal -if bufexists(fnamemodify("actions.json", ":p")) | buffer actions.json | else | edit actions.json | endif -if &buftype ==# 'terminal' - silent file actions.json -endif -balt settings.py -setlocal fdm=manual -setlocal fde=0 -setlocal fmr={{{,}}} -setlocal fdi=# -setlocal fdl=0 -setlocal fml=1 -setlocal fdn=20 -setlocal fen -silent! normal! zE -let &fdl = &fdl -let s:l = 5 - ((2 * winheight(0) + 15) / 31) -if s:l < 1 | let s:l = 1 | endif -keepjumps exe s:l -normal! zt -keepjumps 5 -normal! 021| -wincmd w -exe 'vert 1resize ' . ((&columns * 109 + 163) / 327) -exe 'vert 2resize ' . ((&columns * 108 + 163) / 327) -exe '3resize ' . ((&lines * 32 + 33) / 67) -exe 'vert 3resize ' . ((&columns * 108 + 163) / 327) -exe '4resize ' . ((&lines * 31 + 33) / 67) -exe 'vert 4resize ' . ((&columns * 108 + 163) / 327) -tabnext -edit cli.py -let s:save_splitbelow = &splitbelow -let s:save_splitright = &splitright -set splitbelow splitright -wincmd _ | wincmd | -vsplit -wincmd _ | wincmd | -vsplit -2wincmd h -wincmd w -wincmd w -let &splitbelow = s:save_splitbelow -let &splitright = s:save_splitright -wincmd t -let s:save_winminheight = &winminheight -let s:save_winminwidth = &winminwidth -set winminheight=0 -set winheight=1 -set winminwidth=0 -set winwidth=1 -exe 'vert 1resize ' . ((&columns * 108 + 163) / 327) -exe 'vert 2resize ' . ((&columns * 108 + 163) / 327) -exe 'vert 3resize ' . ((&columns * 109 + 163) / 327) -argglobal -balt src/actions.py -setlocal fdm=manual -setlocal fde=0 -setlocal fmr={{{,}}} -setlocal fdi=# -setlocal fdl=0 -setlocal fml=1 -setlocal fdn=20 -setlocal fen -silent! normal! zE -let &fdl = &fdl -let s:l = 50 - ((49 * winheight(0) + 32) / 64) -if s:l < 1 | let s:l = 1 | endif -keepjumps exe s:l -normal! zt -keepjumps 50 -normal! 09| -wincmd w -argglobal -if bufexists(fnamemodify("src/actions.py", ":p")) | buffer src/actions.py | else | edit src/actions.py | endif -if &buftype ==# 'terminal' - silent file src/actions.py -endif -balt cli.py -setlocal fdm=manual -setlocal fde=0 -setlocal fmr={{{,}}} -setlocal fdi=# -setlocal fdl=0 -setlocal fml=1 -setlocal fdn=20 -setlocal fen -silent! normal! zE -let &fdl = &fdl -let s:l = 16 - ((10 * winheight(0) + 32) / 64) -if s:l < 1 | let s:l = 1 | endif -keepjumps exe s:l -normal! zt -keepjumps 16 -normal! 0 -wincmd w -argglobal -if bufexists(fnamemodify("src/lint.py", ":p")) | buffer src/lint.py | else | edit src/lint.py | endif -if &buftype ==# 'terminal' - silent file src/lint.py -endif -balt cli.py -setlocal fdm=manual -setlocal fde=0 -setlocal fmr={{{,}}} -setlocal fdi=# -setlocal fdl=0 -setlocal fml=1 -setlocal fdn=20 -setlocal fen -silent! normal! zE -let &fdl = &fdl -let s:l = 65 - ((1 * winheight(0) + 32) / 64) -if s:l < 1 | let s:l = 1 | endif -keepjumps exe s:l -normal! zt -keepjumps 65 -normal! 036| -wincmd w -3wincmd w -exe 'vert 1resize ' . ((&columns * 108 + 163) / 327) -exe 'vert 2resize ' . ((&columns * 108 + 163) / 327) -exe 'vert 3resize ' . ((&columns * 109 + 163) / 327) -tabnext -edit src/load.py -let s:save_splitbelow = &splitbelow -let s:save_splitright = &splitright -set splitbelow splitright -wincmd _ | wincmd | -vsplit -wincmd _ | wincmd | -vsplit -wincmd _ | wincmd | -vsplit -3wincmd h -wincmd w -wincmd w -wincmd w -let &splitbelow = s:save_splitbelow -let &splitright = s:save_splitright -wincmd t -let s:save_winminheight = &winminheight -let s:save_winminwidth = &winminwidth -set winminheight=0 -set winheight=1 -set winminwidth=0 -set winwidth=1 -exe 'vert 1resize ' . ((&columns * 81 + 163) / 327) -exe 'vert 2resize ' . ((&columns * 81 + 163) / 327) -exe 'vert 3resize ' . ((&columns * 81 + 163) / 327) -exe 'vert 4resize ' . ((&columns * 81 + 163) / 327) -argglobal -balt tests/test_load.py -setlocal fdm=manual -setlocal fde=0 -setlocal fmr={{{,}}} -setlocal fdi=# -setlocal fdl=0 -setlocal fml=1 -setlocal fdn=20 -setlocal fen -silent! normal! zE -let &fdl = &fdl -let s:l = 67 - ((32 * winheight(0) + 32) / 64) -if s:l < 1 | let s:l = 1 | endif -keepjumps exe s:l -normal! zt -keepjumps 67 -normal! 042| -wincmd w -argglobal -if bufexists(fnamemodify("tests/test_load.py", ":p")) | buffer tests/test_load.py | else | edit tests/test_load.py | endif -if &buftype ==# 'terminal' - silent file tests/test_load.py -endif -balt src/load.py -setlocal fdm=manual -setlocal fde=0 -setlocal fmr={{{,}}} -setlocal fdi=# -setlocal fdl=0 -setlocal fml=1 -setlocal fdn=20 -setlocal fen -silent! normal! zE -let &fdl = &fdl -let s:l = 45 - ((44 * winheight(0) + 32) / 64) -if s:l < 1 | let s:l = 1 | endif -keepjumps exe s:l -normal! zt -keepjumps 45 -normal! 0 -wincmd w -argglobal -if bufexists(fnamemodify("src/utils.py", ":p")) | buffer src/utils.py | else | edit src/utils.py | endif -if &buftype ==# 'terminal' - silent file src/utils.py -endif -balt src/load.py -setlocal fdm=manual -setlocal fde=0 -setlocal fmr={{{,}}} -setlocal fdi=# -setlocal fdl=0 -setlocal fml=1 -setlocal fdn=20 -setlocal fen -silent! normal! zE -let &fdl = &fdl -let s:l = 49 - ((45 * winheight(0) + 32) / 64) -if s:l < 1 | let s:l = 1 | endif -keepjumps exe s:l -normal! zt -keepjumps 49 -normal! 09| -wincmd w -argglobal -if bufexists(fnamemodify("tests/test_utils.py", ":p")) | buffer tests/test_utils.py | else | edit tests/test_utils.py | endif -if &buftype ==# 'terminal' - silent file tests/test_utils.py -endif -balt tests/test_load.py -setlocal fdm=manual -setlocal fde=0 -setlocal fmr={{{,}}} -setlocal fdi=# -setlocal fdl=0 -setlocal fml=1 -setlocal fdn=20 -setlocal fen -silent! normal! zE -let &fdl = &fdl -let s:l = 36 - ((35 * winheight(0) + 32) / 64) -if s:l < 1 | let s:l = 1 | endif -keepjumps exe s:l -normal! zt -keepjumps 36 -normal! 0 -wincmd w -exe 'vert 1resize ' . ((&columns * 81 + 163) / 327) -exe 'vert 2resize ' . ((&columns * 81 + 163) / 327) -exe 'vert 3resize ' . ((&columns * 81 + 163) / 327) -exe 'vert 4resize ' . ((&columns * 81 + 163) / 327) -tabnext -edit src/rule.py -let s:save_splitbelow = &splitbelow -let s:save_splitright = &splitright -set splitbelow splitright -wincmd _ | wincmd | -vsplit -1wincmd h -wincmd w -let &splitbelow = s:save_splitbelow -let &splitright = s:save_splitright -wincmd t -let s:save_winminheight = &winminheight -let s:save_winminwidth = &winminwidth -set winminheight=0 -set winheight=1 -set winminwidth=0 -set winwidth=1 -exe 'vert 1resize ' . ((&columns * 163 + 163) / 327) -exe 'vert 2resize ' . ((&columns * 163 + 163) / 327) -argglobal -balt tests/test_rule.py -setlocal fdm=manual -setlocal fde=0 -setlocal fmr={{{,}}} -setlocal fdi=# -setlocal fdl=0 -setlocal fml=1 -setlocal fdn=20 -setlocal fen -silent! normal! zE -let &fdl = &fdl -let s:l = 41 - ((40 * winheight(0) + 32) / 64) -if s:l < 1 | let s:l = 1 | endif -keepjumps exe s:l -normal! zt -keepjumps 41 -normal! 0 -wincmd w -argglobal -if bufexists(fnamemodify("tests/test_rule.py", ":p")) | buffer tests/test_rule.py | else | edit tests/test_rule.py | endif -if &buftype ==# 'terminal' - silent file tests/test_rule.py -endif -balt tests/fixtures/test-min-incorrect.yaml -setlocal fdm=manual -setlocal fde=0 -setlocal fmr={{{,}}} -setlocal fdi=# -setlocal fdl=0 -setlocal fml=1 -setlocal fdn=20 -setlocal fen -silent! normal! zE -let &fdl = &fdl -let s:l = 133 - ((56 * winheight(0) + 32) / 64) -if s:l < 1 | let s:l = 1 | endif -keepjumps exe s:l -normal! zt -keepjumps 133 -normal! 012| -wincmd w -exe 'vert 1resize ' . ((&columns * 163 + 163) / 327) -exe 'vert 2resize ' . ((&columns * 163 + 163) / 327) -tabnext -edit src/models/step.py -argglobal -balt src/rules/step_approved.py -setlocal fdm=manual -setlocal fde=0 -setlocal fmr={{{,}}} -setlocal fdi=# -setlocal fdl=0 -setlocal fml=1 -setlocal fdn=20 -setlocal fen -silent! normal! zE -let &fdl = &fdl -let s:l = 23 - ((22 * winheight(0) + 32) / 64) -if s:l < 1 | let s:l = 1 | endif -keepjumps exe s:l -normal! zt -keepjumps 23 -normal! 029| -tabnext 2 -set stal=1 -if exists('s:wipebuf') && len(win_findbuf(s:wipebuf)) == 0 && getbufvar(s:wipebuf, '&buftype') isnot# 'terminal' - silent exe 'bwipe ' . s:wipebuf -endif -unlet! s:wipebuf -set winheight=1 winwidth=20 -let &shortmess = s:shortmess_save -let s:sx = expand(":p:r")."x.vim" -if filereadable(s:sx) - exe "source " . fnameescape(s:sx) -endif -let &g:so = s:so_save | let &g:siso = s:siso_save -set hlsearch -doautoall SessionLoadPost -unlet SessionLoad -" vim: set ft=vim : diff --git a/lint-workflow/settings.py b/lint-workflow/settings.py index a8c72cc5..f3fa66b2 100644 --- a/lint-workflow/settings.py +++ b/lint-workflow/settings.py @@ -12,4 +12,4 @@ with open("actions.json", "r", encoding="utf8") as action_file: - approved_actions = json.loads(action_file.read()) + approved_actions = json.load(action_file) From fc9cbf7c932c985a6cf379ecd7a68affca7568d2 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Thu, 11 Jan 2024 15:45:09 -0800 Subject: [PATCH 22/57] Fix unit tests --- lint-workflow/tests/rules/test_job_environment_prefix.py | 4 ++-- lint-workflow/tests/rules/test_name_capitalized.py | 6 +++--- lint-workflow/tests/rules/test_name_exists.py | 1 - lint-workflow/tests/rules/test_pinned_job_runner.py | 4 ++-- lint-workflow/tests/rules/test_step_approved.py | 4 ++-- lint-workflow/tests/rules/test_step_pinned.py | 4 ++-- lint-workflow/tests/test_rule.py | 9 ++------- 7 files changed, 13 insertions(+), 19 deletions(-) diff --git a/lint-workflow/tests/rules/test_job_environment_prefix.py b/lint-workflow/tests/rules/test_job_environment_prefix.py index 87528ffa..c77478f6 100644 --- a/lint-workflow/tests/rules/test_job_environment_prefix.py +++ b/lint-workflow/tests/rules/test_job_environment_prefix.py @@ -26,7 +26,7 @@ def correct_workflow(): steps: - run: echo test """ - return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) + return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) @pytest.fixture @@ -44,7 +44,7 @@ def incorrect_workflow(): steps: - run: echo test """ - return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) + return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) @pytest.fixture diff --git a/lint-workflow/tests/rules/test_name_capitalized.py b/lint-workflow/tests/rules/test_name_capitalized.py index 1a4f3849..a496507d 100644 --- a/lint-workflow/tests/rules/test_name_capitalized.py +++ b/lint-workflow/tests/rules/test_name_capitalized.py @@ -28,7 +28,7 @@ def correct_workflow(): - name: Test run: echo test """ - return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) + return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) @pytest.fixture @@ -47,7 +47,7 @@ def incorrect_workflow(): - name: test run: echo test """ - return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) + return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) @pytest.fixture @@ -63,7 +63,7 @@ def missing_name_workflow(): steps: - run: echo test """ - return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) + return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) @pytest.fixture diff --git a/lint-workflow/tests/rules/test_name_exists.py b/lint-workflow/tests/rules/test_name_exists.py index 3853326a..1634acc4 100644 --- a/lint-workflow/tests/rules/test_name_exists.py +++ b/lint-workflow/tests/rules/test_name_exists.py @@ -1,5 +1,4 @@ import pytest - from ..conftest import FIXTURE_DIR from ..context import src diff --git a/lint-workflow/tests/rules/test_pinned_job_runner.py b/lint-workflow/tests/rules/test_pinned_job_runner.py index 18cd1fc1..59d0b93e 100644 --- a/lint-workflow/tests/rules/test_pinned_job_runner.py +++ b/lint-workflow/tests/rules/test_pinned_job_runner.py @@ -24,7 +24,7 @@ def correct_runner(): steps: - run: echo test """ - return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) + return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) @pytest.fixture @@ -40,7 +40,7 @@ def incorrect_runner(): steps: - run: echo test """ - return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) + return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) @pytest.fixture diff --git a/lint-workflow/tests/rules/test_step_approved.py b/lint-workflow/tests/rules/test_step_approved.py index 7f2d83a7..2c3ba039 100644 --- a/lint-workflow/tests/rules/test_step_approved.py +++ b/lint-workflow/tests/rules/test_step_approved.py @@ -54,7 +54,7 @@ def correct_workflow(): - name: Test Run Action run: echo "test" """ - return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) + return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) @pytest.fixture @@ -74,7 +74,7 @@ def incorrect_workflow(): - name: Out of date action uses: actions/download-artifact@7a1cd3216ca9260cd8022db641d960b1db4d1be4 # v4.0.0 """ - return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) + return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) @pytest.fixture diff --git a/lint-workflow/tests/rules/test_step_pinned.py b/lint-workflow/tests/rules/test_step_pinned.py index c917c769..673985d7 100644 --- a/lint-workflow/tests/rules/test_step_pinned.py +++ b/lint-workflow/tests/rules/test_step_pinned.py @@ -34,7 +34,7 @@ def correct_workflow(): - name: Test Run Action run: echo "test" """ - return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) + return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) @pytest.fixture @@ -57,7 +57,7 @@ def incorrect_workflow(): - name: Test Internal Commit uses: bitwarden/gh-actions/get-keyvault-secrets@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 """ - return WorkflowBuilder.build(yaml=yaml.load(workflow), from_file=False) + return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) @pytest.fixture diff --git a/lint-workflow/tests/test_rule.py b/lint-workflow/tests/test_rule.py index 4e0fa471..176ad19a 100644 --- a/lint-workflow/tests/test_rule.py +++ b/lint-workflow/tests/test_rule.py @@ -5,7 +5,7 @@ from ruamel.yaml import YAML from src.load import WorkflowBuilder -from src.rule import Rule +from src.rule import Rule, RuleExecutionException from src.models import Workflow, Job, Step @@ -65,18 +65,13 @@ def fn(self, obj: Union[Workflow, Job, Step]) -> bool: return obj.name is not None, self.message -class TestException(Exception): - """Test Exception.""" - pass - - class RuleException(Rule): def __init__(self): self.message = "should raise Exception" self.on_fail = "error" def fn(self, obj: Union[Workflow, Job, Step]) -> bool: - raise TestException("test Exception") + raise RuleExecutionException("test Exception") @pytest.fixture(name="step_rule") From 21313638bdd1009e20d62f30465ccd0a584882fa Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 12 Jan 2024 07:59:33 -0800 Subject: [PATCH 23/57] Fix linting issues on job_environment_prefix --- lint-workflow/Taskfile.yml | 8 ++++---- .../src/rules/job_environment_prefix.py | 18 ++++++++++++++--- .../rules/test_job_environment_prefix.py | 20 +++++++++---------- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/lint-workflow/Taskfile.yml b/lint-workflow/Taskfile.yml index 673a01a1..98dea17d 100644 --- a/lint-workflow/Taskfile.yml +++ b/lint-workflow/Taskfile.yml @@ -17,14 +17,14 @@ tasks: silent: true cmds: - pipenv run pytype src - + test:unit: cmds: - pipenv run pytest tests - test:unit: + test:unit:single: cmds: - - pipenv run pytest tests + - pipenv run pytest {{.CLI_ARGS}} test:cov: cmds: @@ -44,4 +44,4 @@ tasks: test:e2e:actions:update: cmds: - - pipenv run python cli.py -v actions update --output test.json + - pipenv run python cli.py -v actions update --output test.json diff --git a/lint-workflow/src/rules/job_environment_prefix.py b/lint-workflow/src/rules/job_environment_prefix.py index 95748277..13a33172 100644 --- a/lint-workflow/src/rules/job_environment_prefix.py +++ b/lint-workflow/src/rules/job_environment_prefix.py @@ -1,4 +1,5 @@ -from typing import Union, Tuple +"""A Rule to enforce prefixes environment variables.""" +from typing import Union, Tuple, List from ..rule import Rule from ..models.job import Job @@ -8,8 +9,19 @@ class RuleJobEnvironmentPrefix(Rule): + """Rule to enforce specific prefixes for environemnt variables. + + Maintaining l + """ def __init__(self, settings: Settings = None) -> None: - self.message: str = f"Job Environment vars should start with and underscore:" + """RuleJobEnvironmentPrefix constructor. + + Args: + settings: + A Settings object that contains any default, overriden, or custom settings + required anywhere in the application. + """ + self.message: str = "Job Environment vars should start with and underscore:" self.on_fail: LintLevels = LintLevels.ERROR self.compatibility: List[Union[Workflow, Job, Step]] = [Job] self.settings: Settings = settings @@ -39,7 +51,7 @@ def fn(self, obj: Job) -> Tuple[bool, str]: correct = True offending_keys = [] - for key, value in obj.env.items(): + for key in obj.env.keys(): if key[0] != "_": offending_keys.append(key) correct = False diff --git a/lint-workflow/tests/rules/test_job_environment_prefix.py b/lint-workflow/tests/rules/test_job_environment_prefix.py index c77478f6..25f03c42 100644 --- a/lint-workflow/tests/rules/test_job_environment_prefix.py +++ b/lint-workflow/tests/rules/test_job_environment_prefix.py @@ -1,18 +1,16 @@ +"""Test src/rules/job_environment_prefix.""" import pytest from ruamel.yaml import YAML -from ..conftest import FIXTURE_DIR -from ..context import src - from src.load import WorkflowBuilder from src.rules.job_environment_prefix import RuleJobEnvironmentPrefix yaml = YAML() -@pytest.fixture -def correct_workflow(): +@pytest.fixture(name="correct_workflow") +def fixture_correct_workflow(): workflow = """\ --- on: @@ -29,8 +27,8 @@ def correct_workflow(): return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) -@pytest.fixture -def incorrect_workflow(): +@pytest.fixture(name="incorrect_workflow") +def fixture_incorrect_workflow(): workflow = """\ --- on: @@ -47,8 +45,8 @@ def incorrect_workflow(): return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) -@pytest.fixture -def rule(): +@pytest.fixture(name="rule") +def fixture_rule(): return RuleJobEnvironmentPrefix() @@ -56,7 +54,7 @@ def test_rule_on_correct_workflow(rule, correct_workflow): obj = correct_workflow.jobs["job-key"] result, message = rule.fn(correct_workflow.jobs["job-key"]) - assert result == True + assert result is True assert message == "" finding = rule.execute(obj) @@ -67,7 +65,7 @@ def test_rule_on_incorrect_workflow(rule, incorrect_workflow): obj = incorrect_workflow.jobs["job-key"] result, message = rule.fn(obj) - assert result == False + assert result is False assert "TEST_ENV" in message finding = rule.execute(obj) From bdc35b8ee99c763d23cc29210f38797f99145b18 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 12 Jan 2024 08:04:57 -0800 Subject: [PATCH 24/57] Fix linting issues for RuleNameCapitalized --- lint-workflow/src/rules/name_capitalized.py | 9 +++ .../tests/rules/test_name_capitalized.py | 56 +++++++++---------- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/lint-workflow/src/rules/name_capitalized.py b/lint-workflow/src/rules/name_capitalized.py index e66417c0..eca419b9 100644 --- a/lint-workflow/src/rules/name_capitalized.py +++ b/lint-workflow/src/rules/name_capitalized.py @@ -1,3 +1,4 @@ +"""Enforce all 'name' values start with a capital letter.""" from typing import Union, Tuple from ..rule import Rule @@ -8,7 +9,15 @@ class RuleNameCapitalized(Rule): + """Rule to enforce all 'name' values start with a capital letter.""" def __init__(self, settings: Settings = None) -> None: + """Contructor for RuleNameCapitalized. + + Args: + settings: + A Settings object that contains any default, overriden, or custom settings + required anywhere in the application. + """ self.message = "name must capitalized" self.on_fail: LintLevels = LintLevels.ERROR self.settings: Settings = settings diff --git a/lint-workflow/tests/rules/test_name_capitalized.py b/lint-workflow/tests/rules/test_name_capitalized.py index a496507d..77dea76b 100644 --- a/lint-workflow/tests/rules/test_name_capitalized.py +++ b/lint-workflow/tests/rules/test_name_capitalized.py @@ -1,18 +1,16 @@ +"""Test src/rules/name_capitalized.py.""" import pytest from ruamel.yaml import YAML -from ..conftest import FIXTURE_DIR -from ..context import src - from src.load import WorkflowBuilder from src.rules.name_capitalized import RuleNameCapitalized yaml = YAML() -@pytest.fixture -def correct_workflow(): +@pytest.fixture(name="correct_workflow") +def fixture_correct_workflow(): workflow = """\ --- name: Test Workflow @@ -31,8 +29,8 @@ def correct_workflow(): return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) -@pytest.fixture -def incorrect_workflow(): +@pytest.fixture(name="incorrect_workflow") +def fixture_incorrect_workflow(): workflow = """\ --- name: test @@ -50,8 +48,8 @@ def incorrect_workflow(): return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) -@pytest.fixture -def missing_name_workflow(): +@pytest.fixture(name="missing_name_workflow") +def fixture_missing_name_workflow(): workflow = """\ --- on: @@ -66,43 +64,43 @@ def missing_name_workflow(): return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) -@pytest.fixture -def rule(): +@pytest.fixture(rule="rule") +def fixture_rule(): return RuleNameCapitalized() def test_rule_on_correct_workflow(rule, correct_workflow): - result, message = rule.fn(correct_workflow) - assert result == True + result, _ = rule.fn(correct_workflow) + assert result is True - result, message = rule.fn(correct_workflow.jobs["job-key"]) - assert result == True + result, _ = rule.fn(correct_workflow.jobs["job-key"]) + assert result is True - result, message = rule.fn(correct_workflow.jobs["job-key"].steps[0]) - assert result == True + result, _ = rule.fn(correct_workflow.jobs["job-key"].steps[0]) + assert result is True def test_rule_on_incorrect_workflow_name(rule, incorrect_workflow): - result, message = rule.fn(incorrect_workflow) - assert result == False + result, _ = rule.fn(incorrect_workflow) + assert result is False def test_rule_on_incorrect_job_name(rule, incorrect_workflow): - result, message = rule.fn(incorrect_workflow.jobs["job-key"]) - assert result == False + result, _ = rule.fn(incorrect_workflow.jobs["job-key"]) + assert result is False def test_rule_on_incorrect_step_name(rule, incorrect_workflow): - result, message = rule.fn(incorrect_workflow.jobs["job-key"].steps[0]) - assert result == False + result, _ = rule.fn(incorrect_workflow.jobs["job-key"].steps[0]) + assert result is False def test_rule_on_missing_names(rule, missing_name_workflow): - result, message = rule.fn(missing_name_workflow) - assert result == True + result, _ = rule.fn(missing_name_workflow) + assert result is True - result, message = rule.fn(missing_name_workflow.jobs["job-key"]) - assert result == True + result, _ = rule.fn(missing_name_workflow.jobs["job-key"]) + assert result is True - result, message = rule.fn(missing_name_workflow.jobs["job-key"].steps[0]) - assert result == True + result, _ = rule.fn(missing_name_workflow.jobs["job-key"].steps[0]) + assert result is True From 9d91c1f7129e2ce9475acddb828a73d9de39069f Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 12 Jan 2024 08:17:37 -0800 Subject: [PATCH 25/57] Fix linting warnings for RuleNameExists --- .../src/rules/job_environment_prefix.py | 13 +++- lint-workflow/src/rules/name_capitalized.py | 9 ++- lint-workflow/src/rules/name_exists.py | 16 +++++ lint-workflow/tests/rules/test_name_exists.py | 69 +++++++++++++------ 4 files changed, 80 insertions(+), 27 deletions(-) diff --git a/lint-workflow/src/rules/job_environment_prefix.py b/lint-workflow/src/rules/job_environment_prefix.py index 13a33172..036e5c04 100644 --- a/lint-workflow/src/rules/job_environment_prefix.py +++ b/lint-workflow/src/rules/job_environment_prefix.py @@ -11,10 +11,19 @@ class RuleJobEnvironmentPrefix(Rule): """Rule to enforce specific prefixes for environemnt variables. - Maintaining l + Automated testing is not easily written for GitHub Action Workflows. CI can also + get complicated really quickly and take up hundreds of lines. All of this can + make it very difficult to debug and troubleshoot, especially when environment + variables can be set in four different places: Workflow level, Job level, Step + level, and inside a shell Step. + + To alleviate some of the pain, we have decided that all Job level environment + variables should be prefixed with an underscore. All Workflow environment + variables are normally at the top of the file and Step level ones are pretty + visible when debugging a shell Step. """ def __init__(self, settings: Settings = None) -> None: - """RuleJobEnvironmentPrefix constructor. + """RuleJobEnvironmentPrefix constructor to override the Rule class. Args: settings: diff --git a/lint-workflow/src/rules/name_capitalized.py b/lint-workflow/src/rules/name_capitalized.py index eca419b9..84b4d948 100644 --- a/lint-workflow/src/rules/name_capitalized.py +++ b/lint-workflow/src/rules/name_capitalized.py @@ -1,4 +1,4 @@ -"""Enforce all 'name' values start with a capital letter.""" +"""A Rule to enforce all 'name' values start with a capital letter.""" from typing import Union, Tuple from ..rule import Rule @@ -9,9 +9,12 @@ class RuleNameCapitalized(Rule): - """Rule to enforce all 'name' values start with a capital letter.""" + """Rule to enforce all 'name' values start with a capital letter. + + A simple standard to help keep uniformity in naming. + """ def __init__(self, settings: Settings = None) -> None: - """Contructor for RuleNameCapitalized. + """Contructor for RuleNameCapitalized to override the Rule class. Args: settings: diff --git a/lint-workflow/src/rules/name_exists.py b/lint-workflow/src/rules/name_exists.py index 165038b0..e3a8920d 100644 --- a/lint-workflow/src/rules/name_exists.py +++ b/lint-workflow/src/rules/name_exists.py @@ -1,3 +1,4 @@ +"""A Rule to enforce that a 'name' exists.""" from typing import Union, Tuple from ..rule import Rule @@ -8,7 +9,22 @@ class RuleNameExists(Rule): + """Rule to enforce a 'name' exists for every object in GitHub Actions. + + For pipeline run troubleshooting and debugging, it is helpful to have a + name to immediately identify a Workflow, Job, or Step while moving between + run and the code. + + It also helps with uniformity of runs. + """ def __init__(self, settings: Settings = None) -> None: + """Contructor for RuleNameCapitalized to override Rule class. + + Args: + settings: + A Settings object that contains any default, overriden, or custom settings + required anywhere in the application. + """ self.message = "name must exist" self.on_fail: LintLevels = LintLevels.ERROR self.settings: Settings = settings diff --git a/lint-workflow/tests/rules/test_name_exists.py b/lint-workflow/tests/rules/test_name_exists.py index 1634acc4..e1e7e6ca 100644 --- a/lint-workflow/tests/rules/test_name_exists.py +++ b/lint-workflow/tests/rules/test_name_exists.py @@ -1,44 +1,69 @@ +"""Test src/rules/name_exists.py.""" import pytest -from ..conftest import FIXTURE_DIR -from ..context import src from src.load import WorkflowBuilder from src.rules.name_exists import RuleNameExists -@pytest.fixture -def correct_workflow(): - return WorkflowBuilder.build(f"{FIXTURE_DIR}/test-min.yaml") +@pytest.fixture(name="correct_workflow") +def fixture_correct_workflow(): + workflow = """\ +--- +name: Test Workflow +on: + workflow_dispatch: -@pytest.fixture -def incorrect_workflow(): - return WorkflowBuilder.build(f"{FIXTURE_DIR}/test-min-incorrect.yaml") +jobs: + job-key: + name: Test + runs-on: ubuntu-latest + steps: + - name: Test + run: echo test +""" + return WorkflowBuilder.build(workflow=workflow, from_file=False) -@pytest.fixture -def rule(): +@pytest.fixture(name="incorrect_workflow") +def fixture_incorrect_workflow(): + workflow = """\ +--- +on: + workflow_dispatch: + +jobs: + job-key: + runs-on: ubuntu-latest + steps: + - run: echo test +""" + return WorkflowBuilder.build(workflow=workflow, from_file=False) + + +@pytest.fixture(name="rule") +def fixture_rule(): return RuleNameExists() def test_rule_on_correct_workflow(rule, correct_workflow): - result, message = rule.fn(correct_workflow) - assert result == True + result, _ = rule.fn(correct_workflow) + assert result is True - result, message = rule.fn(correct_workflow.jobs["job-key"]) - assert result == True + result, _ = rule.fn(correct_workflow.jobs["job-key"]) + assert result is True - result, message = rule.fn(correct_workflow.jobs["job-key"].steps[0]) - assert result == True + result, _ = rule.fn(correct_workflow.jobs["job-key"].steps[0]) + assert result is True def test_rule_on_incorrect_workflow(rule, incorrect_workflow): print(f"Workflow name: {incorrect_workflow.name}") - result, message = rule.fn(incorrect_workflow) - assert result == False + result, _ = rule.fn(incorrect_workflow) + assert result is False - result, message = rule.fn(incorrect_workflow.jobs["job-key"]) - assert result == False + result, _ = rule.fn(incorrect_workflow.jobs["job-key"]) + assert result is False - result, message = rule.fn(incorrect_workflow.jobs["job-key"].steps[0]) - assert result == False + result, _ = rule.fn(incorrect_workflow.jobs["job-key"].steps[0]) + assert result is False From 527c41bbbaff0a2d9e8ef9a419ad4619ce1acbe4 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 12 Jan 2024 08:28:24 -0800 Subject: [PATCH 26/57] Fix unit tests. Time to add some githooks... --- lint-workflow/tests/rules/test_name_capitalized.py | 2 +- lint-workflow/tests/rules/test_name_exists.py | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lint-workflow/tests/rules/test_name_capitalized.py b/lint-workflow/tests/rules/test_name_capitalized.py index 77dea76b..41120175 100644 --- a/lint-workflow/tests/rules/test_name_capitalized.py +++ b/lint-workflow/tests/rules/test_name_capitalized.py @@ -64,7 +64,7 @@ def fixture_missing_name_workflow(): return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) -@pytest.fixture(rule="rule") +@pytest.fixture(name="rule") def fixture_rule(): return RuleNameCapitalized() diff --git a/lint-workflow/tests/rules/test_name_exists.py b/lint-workflow/tests/rules/test_name_exists.py index e1e7e6ca..2d2ed977 100644 --- a/lint-workflow/tests/rules/test_name_exists.py +++ b/lint-workflow/tests/rules/test_name_exists.py @@ -1,10 +1,15 @@ """Test src/rules/name_exists.py.""" import pytest +from ruamel.yaml import YAML + from src.load import WorkflowBuilder from src.rules.name_exists import RuleNameExists +yaml = YAML() + + @pytest.fixture(name="correct_workflow") def fixture_correct_workflow(): workflow = """\ @@ -22,7 +27,7 @@ def fixture_correct_workflow(): - name: Test run: echo test """ - return WorkflowBuilder.build(workflow=workflow, from_file=False) + return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) @pytest.fixture(name="incorrect_workflow") @@ -38,7 +43,7 @@ def fixture_incorrect_workflow(): steps: - run: echo test """ - return WorkflowBuilder.build(workflow=workflow, from_file=False) + return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) @pytest.fixture(name="rule") From b52eaab0c54307607216bf2765dd01660f153b07 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 12 Jan 2024 10:22:38 -0800 Subject: [PATCH 27/57] Update with linter findings for RuleStepUsesApproved --- .../src/rules/job_environment_prefix.py | 2 +- lint-workflow/src/rules/name_capitalized.py | 4 +- lint-workflow/src/rules/name_exists.py | 4 +- lint-workflow/src/rules/pinned_job_runner.py | 18 ++++++++- lint-workflow/src/rules/step_approved.py | 28 +++++++------ lint-workflow/src/rules/step_pinned.py | 2 +- .../tests/rules/test_pinned_job_runner.py | 24 +++++------ .../tests/rules/test_step_approved.py | 40 +++++++++---------- 8 files changed, 67 insertions(+), 55 deletions(-) diff --git a/lint-workflow/src/rules/job_environment_prefix.py b/lint-workflow/src/rules/job_environment_prefix.py index 036e5c04..e631ce2e 100644 --- a/lint-workflow/src/rules/job_environment_prefix.py +++ b/lint-workflow/src/rules/job_environment_prefix.py @@ -1,10 +1,10 @@ """A Rule to enforce prefixes environment variables.""" from typing import Union, Tuple, List -from ..rule import Rule from ..models.job import Job from ..models.workflow import Workflow from ..models.step import Step +from ..rule import Rule from ..utils import LintLevels, Settings diff --git a/lint-workflow/src/rules/name_capitalized.py b/lint-workflow/src/rules/name_capitalized.py index 84b4d948..4e24bbfc 100644 --- a/lint-workflow/src/rules/name_capitalized.py +++ b/lint-workflow/src/rules/name_capitalized.py @@ -1,10 +1,10 @@ """A Rule to enforce all 'name' values start with a capital letter.""" -from typing import Union, Tuple +from typing import Tuple, Union -from ..rule import Rule from ..models.workflow import Workflow from ..models.job import Job from ..models.step import Step +from ..rule import Rule from ..utils import LintLevels, Settings diff --git a/lint-workflow/src/rules/name_exists.py b/lint-workflow/src/rules/name_exists.py index e3a8920d..021218d3 100644 --- a/lint-workflow/src/rules/name_exists.py +++ b/lint-workflow/src/rules/name_exists.py @@ -1,10 +1,10 @@ """A Rule to enforce that a 'name' exists.""" -from typing import Union, Tuple +from typing import Tuple, Union -from ..rule import Rule from ..models.workflow import Workflow from ..models.job import Job from ..models.step import Step +from ..rule import Rule from ..utils import LintLevels, Settings diff --git a/lint-workflow/src/rules/pinned_job_runner.py b/lint-workflow/src/rules/pinned_job_runner.py index d2897f48..135baea2 100644 --- a/lint-workflow/src/rules/pinned_job_runner.py +++ b/lint-workflow/src/rules/pinned_job_runner.py @@ -1,14 +1,28 @@ -from typing import Union, Tuple +"""A Rule to enforce pinning runners to a specific OS version.""" +from typing import List, Tuple, Union -from ..rule import Rule from ..models.job import Job from ..models.workflow import Workflow from ..models.step import Step +from ..rule import Rule from ..utils import LintLevels, Settings class RuleJobRunnerVersionPinned(Rule): + """Rule to enforce pinned Runner OS versions. + + `*-latest` versions updating without knowing has broken all of our worklfows + in the past. To avoid this and prevent a single event from breaking the majority of + our pipelines, we pin the versions. + """ def __init__(self, settings: Settings = None) -> None: + """Constructor for RuleJobRunnerVersionPinned to override Rule class. + + Args: + settings: + A Settings object that contains any default, overriden, or custom settings + required anywhere in the application. + """ self.message = "Workflow runner must be pinned" self.on_fail: LintLevels = LintLevels.ERROR self.compatibility: List[Union[Workflow, Job, Step]] = [Job] diff --git a/lint-workflow/src/rules/step_approved.py b/lint-workflow/src/rules/step_approved.py index b184059b..276ea1ac 100644 --- a/lint-workflow/src/rules/step_approved.py +++ b/lint-workflow/src/rules/step_approved.py @@ -1,15 +1,22 @@ -from typing import Union, Tuple +"""A Rule to enforce the use of a list of pre-approved Actions.""" +from typing import List, Tuple, Union -from ..rule import Rule from ..models.job import Job from ..models.workflow import Workflow from ..models.step import Step +from ..rule import Rule from ..utils import LintLevels, Settings class RuleStepUsesApproved(Rule): + """Rule to enforce that all Actions have been pre-approved. + + To limit the surface area of a supply chain attack in our pipelines, all Actions + are required to pass a security review and be added to the pre-approved list to + check against. + """ def __init__(self, settings: Settings = None) -> None: - self.message = f"error" + self.message = "error" self.on_fail: LintLevels = LintLevels.WARNING self.compatibility: List[Union[Workflow, Job, Step]] = [Step] self.settings: Settings = settings @@ -69,19 +76,14 @@ def fn(self, obj: Step) -> Tuple[bool, str]: if self.skip(obj): return True, "" - path, hash = obj.uses.split("@") - # Actions in bitwarden/gh-actions are auto-approved - if not path in self.settings.approved_actions: - return ( - False, - ( - f"New Action detected: {path}\n" - "For security purposes, actions must be reviewed and on the pre-approved list" - ), + if obj.uses and not obj.uses_path in self.settings.approved_actions: + return False, ( + f"New Action detected: {obj.uses_path}\nFor security purposes, " + "actions must be reviewed and on the pre-approved list" ) - action = self.settings.approved_actions[path] + action = self.settings.approved_actions[obj.uses_path] if obj.uses_version != action.version or obj.uses_ref != action.sha: return False, ( diff --git a/lint-workflow/src/rules/step_pinned.py b/lint-workflow/src/rules/step_pinned.py index 2f49cfd7..a2a1efd9 100644 --- a/lint-workflow/src/rules/step_pinned.py +++ b/lint-workflow/src/rules/step_pinned.py @@ -1,9 +1,9 @@ from typing import Union, Tuple -from ..rule import Rule from ..models.job import Job from ..models.workflow import Workflow from ..models.step import Step +from ..rule import Rule from ..utils import LintLevels, Settings diff --git a/lint-workflow/tests/rules/test_pinned_job_runner.py b/lint-workflow/tests/rules/test_pinned_job_runner.py index 59d0b93e..e3facfad 100644 --- a/lint-workflow/tests/rules/test_pinned_job_runner.py +++ b/lint-workflow/tests/rules/test_pinned_job_runner.py @@ -1,18 +1,16 @@ +"""Test src/rules/pinned_job_runner.py.""" import pytest from ruamel.yaml import YAML -from ..conftest import FIXTURE_DIR -from ..context import src - from src.load import WorkflowBuilder from src.rules.pinned_job_runner import RuleJobRunnerVersionPinned yaml = YAML() -@pytest.fixture -def correct_runner(): +@pytest.fixture(name="correct_runner") +def fixture_correct_runner(): workflow = """\ --- on: @@ -27,8 +25,8 @@ def correct_runner(): return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) -@pytest.fixture -def incorrect_runner(): +@pytest.fixture(name="incorrect_runner") +def fixture_incorrect_runner(): workflow = """\ --- on: @@ -43,16 +41,16 @@ def incorrect_runner(): return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) -@pytest.fixture -def rule(): +@pytest.fixture(name="rule") +def fixture_rule(): return RuleJobRunnerVersionPinned() def test_rule_on_correct_runner(rule, correct_runner): - result, message = rule.fn(correct_runner.jobs["job-key"]) - assert result == True + result, _ = rule.fn(correct_runner.jobs["job-key"]) + assert result is True def test_rule_on_incorrect_runner(rule, incorrect_runner): - result, message = rule.fn(incorrect_runner.jobs["job-key"]) - assert result == False + result, _ = rule.fn(incorrect_runner.jobs["job-key"]) + assert result is False diff --git a/lint-workflow/tests/rules/test_step_approved.py b/lint-workflow/tests/rules/test_step_approved.py index 2c3ba039..c343fecd 100644 --- a/lint-workflow/tests/rules/test_step_approved.py +++ b/lint-workflow/tests/rules/test_step_approved.py @@ -1,10 +1,8 @@ +"""Test src/rules/step_approved.py.""" import pytest from ruamel.yaml import YAML -from ..conftest import FIXTURE_DIR -from ..context import src - from src.load import WorkflowBuilder from src.rules.step_approved import RuleStepUsesApproved from src.utils import Settings @@ -13,8 +11,8 @@ yaml = YAML() -@pytest.fixture -def settings(): +@pytest.fixture(name="settings") +def fixture_settings(): return Settings( approved_actions={ "actions/checkout": { @@ -31,8 +29,8 @@ def settings(): ) -@pytest.fixture -def correct_workflow(): +@pytest.fixture(name="correct_workflow") +def fixture_correct_workflow(): workflow = """\ --- on: @@ -57,8 +55,8 @@ def correct_workflow(): return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) -@pytest.fixture -def incorrect_workflow(): +@pytest.fixture(name="incorrect_workflow") +def fixture_incorrect_workflow(): workflow = """\ --- on: @@ -77,32 +75,32 @@ def incorrect_workflow(): return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) -@pytest.fixture -def rule(settings): +@pytest.fixture(name="rule") +def fixture_rule(settings): return RuleStepUsesApproved(settings=settings) def test_rule_on_correct_workflow(rule, correct_workflow): - result, message = rule.fn(correct_workflow.jobs["job-key"].steps[0]) - assert result == True + result, _ = rule.fn(correct_workflow.jobs["job-key"].steps[0]) + assert result is True - result, message = rule.fn(correct_workflow.jobs["job-key"].steps[1]) - assert result == True + result, _ = rule.fn(correct_workflow.jobs["job-key"].steps[1]) + assert result is True - result, message = rule.fn(correct_workflow.jobs["job-key"].steps[2]) - assert result == True + result, _ = rule.fn(correct_workflow.jobs["job-key"].steps[2]) + assert result is True - result, message = rule.fn(correct_workflow.jobs["job-key"].steps[3]) - assert result == True + result, _ = rule.fn(correct_workflow.jobs["job-key"].steps[3]) + assert result is True def test_rule_on_incorrect_workflow(rule, incorrect_workflow): result, message = rule.fn(incorrect_workflow.jobs["job-key"].steps[0]) - assert result == False + assert result is False assert "New Action detected" in message result, message = rule.fn(incorrect_workflow.jobs["job-key"].steps[1]) - assert result == False + assert result is False assert "Action is out of date" in message From 057c2c5b05f8520eba693e51b1f87f583855752a Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 12 Jan 2024 11:14:52 -0800 Subject: [PATCH 28/57] Remove 'message' attribute from Rule base class. Fix linting findings for RuleStepPinned --- lint-workflow/src/rule.py | 5 +-- .../src/rules/job_environment_prefix.py | 2 +- lint-workflow/src/rules/step_approved.py | 8 +++- lint-workflow/src/rules/step_pinned.py | 29 ++++++++++++-- lint-workflow/tests/rules/test_step_pinned.py | 38 +++++++++---------- 5 files changed, 53 insertions(+), 29 deletions(-) diff --git a/lint-workflow/src/rule.py b/lint-workflow/src/rule.py index 23932544..f27ee323 100644 --- a/lint-workflow/src/rule.py +++ b/lint-workflow/src/rule.py @@ -15,8 +15,7 @@ class RuleExecutionException(Exception): class Rule: """Base class of a Rule to extend to create a linting Rule.""" - message: str = "error" - on_fail: str = "error" + on_fail: LintLevels = LintLevels.ERROR compatibility: List[Union[Workflow, Job, Step]] = [Workflow, Job, Step] settings: Settings = None @@ -30,7 +29,7 @@ def fn(self, obj: Union[Workflow, Job, Step]) -> bool: Returns: The success/failure of the result of the Rule ran on the input. """ - return False, f"{obj.name}: {self.message}" + return False, f"{obj.name}: " def build_lint_message(self, message: str, obj: Union[Workflow, Job, Step]) -> str: """Build the lint failure message. diff --git a/lint-workflow/src/rules/job_environment_prefix.py b/lint-workflow/src/rules/job_environment_prefix.py index e631ce2e..a864c063 100644 --- a/lint-workflow/src/rules/job_environment_prefix.py +++ b/lint-workflow/src/rules/job_environment_prefix.py @@ -30,7 +30,7 @@ def __init__(self, settings: Settings = None) -> None: A Settings object that contains any default, overriden, or custom settings required anywhere in the application. """ - self.message: str = "Job Environment vars should start with and underscore:" + self.message: str = "Job environment vars should start with and underscore:" self.on_fail: LintLevels = LintLevels.ERROR self.compatibility: List[Union[Workflow, Job, Step]] = [Job] self.settings: Settings = settings diff --git a/lint-workflow/src/rules/step_approved.py b/lint-workflow/src/rules/step_approved.py index 276ea1ac..3ada71df 100644 --- a/lint-workflow/src/rules/step_approved.py +++ b/lint-workflow/src/rules/step_approved.py @@ -16,7 +16,13 @@ class RuleStepUsesApproved(Rule): check against. """ def __init__(self, settings: Settings = None) -> None: - self.message = "error" + """Constructor for RuleStepUsesApproved to override Rule class. + + Args: + settings: + A Settings object that contains any default, overriden, or custom settings + required anywhere in the application. + """ self.on_fail: LintLevels = LintLevels.WARNING self.compatibility: List[Union[Workflow, Job, Step]] = [Step] self.settings: Settings = settings diff --git a/lint-workflow/src/rules/step_pinned.py b/lint-workflow/src/rules/step_pinned.py index a2a1efd9..cdbfbf82 100644 --- a/lint-workflow/src/rules/step_pinned.py +++ b/lint-workflow/src/rules/step_pinned.py @@ -1,4 +1,5 @@ -from typing import Union, Tuple +"""A Rule to enforce Actions are pinned correctly.""" +from typing import List, Tuple, Union from ..models.job import Job from ..models.workflow import Workflow @@ -8,8 +9,28 @@ class RuleStepUsesPinned(Rule): + """Rule to contain the enforcement logic for pinning Actions versions. + + Definition of Internal Action: + An Action that exists in the `bitwarden/gh-actions` GitHub Repository. + + For any external Action (any Action that does not fit the above definition of + an Internal Action), to mitigate the risks of supply chain attacks in our CI + pipelines, we pin any use of an Action to a specific hash that has been verified + and pre-approved after a security audit of the version of the Action. + + All Internl Actions, should be pinned to 'main'. This prevents Renovate from + spamming a bunch of PRs across all of our repos when `bitwarden/gh-actions` is + updated. + """ def __init__(self, settings: Settings = None) -> None: - self.message = f"error" + """Constructor for RuleStepUsesPinned to override base Rule. + + Args: + settings: + A Settings object that contains any default, overriden, or custom settings + required anywhere in the application. + """ self.on_fail: LintLevels = LintLevels.ERROR self.compatibility: List[Union[Workflow, Job, Step]] = [Step] self.settings: Settings = settings @@ -68,10 +89,10 @@ def fn(self, obj: Step) -> Tuple[bool, str]: try: int(ref, 16) - except: + except ValueError: return False, "Please pin the action to a commit sha" if len(ref) != 40: - return False, f"Please use the full commit sha to pin the action" + return False, "Please use the full commit sha to pin the action" return True, "" diff --git a/lint-workflow/tests/rules/test_step_pinned.py b/lint-workflow/tests/rules/test_step_pinned.py index 673985d7..101d7922 100644 --- a/lint-workflow/tests/rules/test_step_pinned.py +++ b/lint-workflow/tests/rules/test_step_pinned.py @@ -1,18 +1,16 @@ +"""Test src/rules/step_pinned.py.""" import pytest from ruamel.yaml import YAML -from ..conftest import FIXTURE_DIR -from ..context import src - from src.load import WorkflowBuilder from src.rules.step_pinned import RuleStepUsesPinned yaml = YAML() -@pytest.fixture -def correct_workflow(): +@pytest.fixture(name="correct_workflow") +def fixture_correct_workflow(): workflow = """\ --- on: @@ -37,8 +35,8 @@ def correct_workflow(): return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) -@pytest.fixture -def incorrect_workflow(): +@pytest.fixture(name="incorrect_workflow") +def fixture_incorrect_workflow(): workflow = """\ --- on: @@ -60,40 +58,40 @@ def incorrect_workflow(): return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) -@pytest.fixture -def rule(): +@pytest.fixture(name="rule") +def fixture_rule(): return RuleStepUsesPinned() def test_rule_on_correct_workflow(rule, correct_workflow): - result, message = rule.fn(correct_workflow.jobs["job-key"].steps[0]) - assert result == True + result, _ = rule.fn(correct_workflow.jobs["job-key"].steps[0]) + assert result is True - result, message = rule.fn(correct_workflow.jobs["job-key"].steps[1]) - assert result == True + result, _ = rule.fn(correct_workflow.jobs["job-key"].steps[1]) + assert result is True - result, message = rule.fn(correct_workflow.jobs["job-key"].steps[2]) - assert result == True + result, _ = rule.fn(correct_workflow.jobs["job-key"].steps[2]) + assert result is True - result, message = rule.fn(correct_workflow.jobs["job-key"].steps[3]) - assert result == True + result, _ = rule.fn(correct_workflow.jobs["job-key"].steps[3]) + assert result is True def test_rule_on_incorrect_workflow_external_branch(rule, incorrect_workflow): result, message = rule.fn(incorrect_workflow.jobs["job-key"].steps[0]) - assert result == False + assert result is False assert "Please pin the action" in message def test_rule_on_incorrect_workflow_hex(rule, incorrect_workflow): result, message = rule.fn(incorrect_workflow.jobs["job-key"].steps[1]) - assert result == False + assert result is False assert "Please use the full commit sha" in message def test_rule_on_incorrect_workflow_internal_commit(rule, incorrect_workflow): result, message = rule.fn(incorrect_workflow.jobs["job-key"].steps[2]) - assert result == False + assert result is False assert "Please pin to main" in message From 40925be427cc8ae25c66e93dee1cdbd218f16a7b Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 12 Jan 2024 11:36:24 -0800 Subject: [PATCH 29/57] Fix linter findings --- lint-workflow/src/models/job.py | 8 ++++++++ lint-workflow/src/models/step.py | 8 ++++++++ lint-workflow/src/models/workflow.py | 18 ++++++++++++++---- lint-workflow/tests/test_job.py | 24 ++++++++++++++---------- lint-workflow/tests/test_step.py | 26 ++++++++++++++------------ lint-workflow/tests/test_workflow.py | 25 +++++++++++++------------ 6 files changed, 71 insertions(+), 38 deletions(-) diff --git a/lint-workflow/src/models/job.py b/lint-workflow/src/models/job.py index 825a0838..6f5ccbca 100644 --- a/lint-workflow/src/models/job.py +++ b/lint-workflow/src/models/job.py @@ -1,3 +1,4 @@ +"""Representation for a job in a GitHub Action workflow.""" from dataclasses import dataclass, field from typing import List, Optional, Self @@ -10,6 +11,12 @@ @dataclass_json(undefined=Undefined.EXCLUDE) @dataclass class Job: + """Represents a job in a GitHub Action workflow. + + This object contains all of the data that is required to run the current linting + Rules against. If a new Rule requies a key that is missing, the attribute should + be added to this class to make it available for use in linting. + """ runs_on: str = field(metadata=config(field_name="runs-on")) key: Optional[str] = None name: Optional[str] = None @@ -18,6 +25,7 @@ class Job: @classmethod def init(cls, key: str, data: CommentedMap) -> Self: + """Custom dataclass constructor to map job data to a Job.""" new_job = cls.from_dict(data) new_job.key = key diff --git a/lint-workflow/src/models/step.py b/lint-workflow/src/models/step.py index aed2191b..ceabd7b5 100644 --- a/lint-workflow/src/models/step.py +++ b/lint-workflow/src/models/step.py @@ -1,3 +1,4 @@ +"""Representation for a job step in a GitHub Action workflow.""" from dataclasses import dataclass, field from typing import Optional, Self @@ -8,6 +9,12 @@ @dataclass_json(undefined=Undefined.EXCLUDE) @dataclass class Step: + """Represents a step in a GitHub Action workflow job. + + This object contains all of the data that is required to run the current linting + Rules against. If a new Rule requies a key that is missing, the attribute should + be added to this class to make it available for use in linting. + """ key: Optional[int] = None job: Optional[str] = None name: Optional[str] = None @@ -24,6 +31,7 @@ class Step: @classmethod def init(cls, idx: int, job: str, data: CommentedMap) -> Self: + """Custom dataclass constructor to map a job step data to a Step.""" new_step = cls.from_dict(data) new_step.key = idx diff --git a/lint-workflow/src/models/workflow.py b/lint-workflow/src/models/workflow.py index 859f4619..f8a52b2d 100644 --- a/lint-workflow/src/models/workflow.py +++ b/lint-workflow/src/models/workflow.py @@ -1,7 +1,8 @@ -from dataclasses import dataclass, field -from typing import Optional +"""Representation for an entire GitHub Action workflow.""" +from dataclasses import dataclass +from typing import Dict, Optional -from dataclasses_json import config, dataclass_json, Undefined +from dataclasses_json import dataclass_json, Undefined from ruamel.yaml.comments import CommentedMap from src.models.job import Job @@ -10,7 +11,16 @@ @dataclass_json(undefined=Undefined.EXCLUDE) @dataclass class Workflow: + """Represents an entire workflow in a GitHub Action workflow. + + This object contains all of the data that is required to run the current linting + Rules against. If a new Rule requies a key that is missing, the attribute should + be added to this class to make it available for use in linting. + + See src/models/job.py for an example if the key in the workflow data does not map + one-to-one in the model (ex. 'with' => 'uses_with') + """ key: str = "" name: Optional[str] = None on: Optional[CommentedMap] = None - jobs: Optional[CommentedMap] = None + jobs: Optional[Dict[str, Job]] = None diff --git a/lint-workflow/tests/test_job.py b/lint-workflow/tests/test_job.py index aafb1527..b01aa143 100644 --- a/lint-workflow/tests/test_job.py +++ b/lint-workflow/tests/test_job.py @@ -1,33 +1,37 @@ -import json +"""Test src/models/job.py.""" import pytest from .context import src +from src.models.job import Job +from src.models.step import Step +from src.models.workflow import Workflow -@pytest.fixture -def default_job_data(): + +@pytest.fixture(name="default_job_data") +def fixture_default_job_data(): return { "name": "Test", "runs-on": "ubuntu-latest", - "steps": [src.models.Step(run="echo stub")], + "steps": [Step(run="echo stub")], } -@pytest.fixture -def default_job(default_job_data): - return src.models.Job.init("default-job", default_job_data) +@pytest.fixture(name="default_job") +def fixture_default_job(default_job_data): + return Job.init("default-job", default_job_data) def test_job_default(default_job): assert default_job.key == "default-job" assert default_job.name == "Test" assert default_job.runs_on == "ubuntu-latest" - assert default_job.env == None + assert default_job.env is None assert len(default_job.steps) == 1 def test_job_extra_kwargs(default_job_data): - job = src.models.Job.init("test-job", {"extra": "test", **default_job_data}) + job = Job.init("test-job", {"extra": "test", **default_job_data}) - with pytest.raises(Exception) as e_info: + with pytest.raises(Exception): assert job.extra == "test" diff --git a/lint-workflow/tests/test_step.py b/lint-workflow/tests/test_step.py index 4e297a07..485c7b16 100644 --- a/lint-workflow/tests/test_step.py +++ b/lint-workflow/tests/test_step.py @@ -1,24 +1,26 @@ +"""Test src/models/step.py.""" import json import pytest from ruamel.yaml import YAML -from .context import src +from src.models.step import Step -@pytest.fixture -def default_step(): + +@pytest.fixture(name="default_step") +def fixture_default_step(): step_str = """\ name: Default Step run: echo "test" """ yaml = YAML() step_yaml = yaml.load(step_str) - return src.models.Step.init(0, "default", step_yaml) + return Step.init(0, "default", step_yaml) -@pytest.fixture -def uses_step(): +@pytest.fixture(name="uses_step") +def fixture_uses_step(): step_str = """\ name: Download Artifacts uses: bitwarden/download-artifacts@main # v1.0.0 @@ -31,26 +33,26 @@ def uses_step(): """ yaml = YAML() step_yaml = yaml.load(step_str) - return src.models.Step.init(0, "default", step_yaml) + return Step.init(0, "default", step_yaml) def test_step_default(default_step): assert default_step.key == 0 assert default_step.job == "default" assert default_step.name == "Default Step" - assert default_step.env == None - assert default_step.uses == None - assert default_step.uses_with == None + assert default_step.env is None + assert default_step.uses is None + assert default_step.uses_with is None assert default_step.run == 'echo "test"' def test_step_no_keyword_field(default_step): - assert default_step.uses_with == None + assert default_step.uses_with is None assert "uses_with" not in default_step.to_json() def test_step_extra_kwargs(default_step): - with pytest.raises(Exception) as e_info: + with pytest.raises(Exception): assert default_step.extra == "test" diff --git a/lint-workflow/tests/test_workflow.py b/lint-workflow/tests/test_workflow.py index 0e198e41..3e765fee 100644 --- a/lint-workflow/tests/test_workflow.py +++ b/lint-workflow/tests/test_workflow.py @@ -1,37 +1,38 @@ -import json +"""Test src/models/workflow.py.""" import pytest -from .conftest import FIXTURE_DIR -from .context import src +from src.models.job import Job +from src.models.step import Step +from src.models.workflow import Workflow -@pytest.fixture -def workflow_default_data(): +@pytest.fixture(name="default_workflow_data") +def fixture_default_workflow_data(): return { "name": "Test Workflow", "on": {}, "jobs": { - "job-key": src.models.Job.from_dict( + "job-key": Job.from_dict( { "name": "Test", "runs-on": "ubuntu-latest", - "steps": [src.models.Step.from_dict({"run": "echo stub"})], + "steps": [Step.from_dict({"run": "echo stub"})], } ) }, } -def test_workflow_default(workflow_default_data): - workflow = src.models.Workflow(**workflow_default_data) +def test_workflow_default(default_workflow_data): + workflow = Workflow(**default_workflow_data) assert workflow.name == "Test Workflow" assert len(workflow.on.keys()) == 0 assert len(workflow.jobs.keys()) == 1 -def test_workflow_extra_kwargs(workflow_default_data): - workflow = src.models.Workflow.from_dict({"extra": "test", **workflow_default_data}) +def test_workflow_extra_kwargs(default_workflow_data): + workflow = Workflow.from_dict({"extra": "test", **default_workflow_data}) - with pytest.raises(Exception) as e_info: + with pytest.raises(Exception): assert workflow.extra == "test" From 8ca7b95c7b0f8f17473ecaeb3b88dad402bae5b2 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 12 Jan 2024 12:23:48 -0800 Subject: [PATCH 30/57] Finish linter finding cleanup --- lint-workflow/README.md | 10 +++++----- lint-workflow/src/__init__.py | 2 -- lint-workflow/src/models/__init__.py | 3 --- lint-workflow/tests/configs.py | 1 - lint-workflow/tests/conftest.py | 2 ++ lint-workflow/tests/context.py | 6 ------ lint-workflow/tests/test_job.py | 3 --- lint-workflow/tests/test_lint.py | 9 +++------ lint-workflow/tests/test_rule.py | 4 +++- lint-workflow/tests/test_step.py | 1 - 10 files changed, 13 insertions(+), 28 deletions(-) delete mode 100644 lint-workflow/tests/configs.py delete mode 100644 lint-workflow/tests/context.py diff --git a/lint-workflow/README.md b/lint-workflow/README.md index f4d65a0d..4134ac42 100644 --- a/lint-workflow/README.md +++ b/lint-workflow/README.md @@ -36,9 +36,9 @@ pipenv shell ### Testing -All built-in `src/rules` should have 100% code coverage and we should shoot for an overall coverage of 80%+. -We are lax on the -[imperative shell](https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell) +All built-in `src/rules` should have 100% code coverage and we should shoot for an overall coverage of 80%+. +We are lax on the +[imperative shell](https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell) (code interacting with other systems; ie. disk, network, etc), but we strive to maintain a high coverage over the funcationl core (objects and models). @@ -59,7 +59,7 @@ black . ### Linting -We loosely use [Google's Python style guide](https://google.github.io/styleguide/pyguide.html), but yield to +We loosely use [Google's Python style guide](https://google.github.io/styleguide/pyguide.html), but yield to `black` when there is a conflict ``` @@ -72,7 +72,7 @@ pylint --rcfile pylintrc src/ tests/ A new Rule is created by extending the Rule base class and overriding the `fn(obj: Union[Workflow, Job, Step])` method. Available attributes of `Workflows`, `Jobs` and `Steps` can be found in their definitons under `src/models`. -For a simple example, we'll take a look at enforcing the existence of the `name` key in a Job. This is already done by +For a simple example, we'll take a look at enforcing the existence of the `name` key in a Job. This is already done by default with the src.rules.name_exists.RuleNameExists, but provides a simple enough example to walk through. ```python diff --git a/lint-workflow/src/__init__.py b/lint-workflow/src/__init__.py index df8c0a2d..e69de29b 100644 --- a/lint-workflow/src/__init__.py +++ b/lint-workflow/src/__init__.py @@ -1,2 +0,0 @@ -from .models import * -from .load import * diff --git a/lint-workflow/src/models/__init__.py b/lint-workflow/src/models/__init__.py index b564f68a..e69de29b 100644 --- a/lint-workflow/src/models/__init__.py +++ b/lint-workflow/src/models/__init__.py @@ -1,3 +0,0 @@ -from .step import * -from .job import * -from .workflow import * diff --git a/lint-workflow/tests/configs.py b/lint-workflow/tests/configs.py deleted file mode 100644 index eec5f0c7..00000000 --- a/lint-workflow/tests/configs.py +++ /dev/null @@ -1 +0,0 @@ -FIXTURES_DIR = "./tests/fixtures" diff --git a/lint-workflow/tests/conftest.py b/lint-workflow/tests/conftest.py index eb3c64df..89ebcc46 100644 --- a/lint-workflow/tests/conftest.py +++ b/lint-workflow/tests/conftest.py @@ -1 +1,3 @@ +"""Shared configation for tests.""" + FIXTURE_DIR = "./tests/fixtures" diff --git a/lint-workflow/tests/context.py b/lint-workflow/tests/context.py deleted file mode 100644 index 7d326ca8..00000000 --- a/lint-workflow/tests/context.py +++ /dev/null @@ -1,6 +0,0 @@ -import sys -import os - -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) - -import src diff --git a/lint-workflow/tests/test_job.py b/lint-workflow/tests/test_job.py index b01aa143..eb6f879b 100644 --- a/lint-workflow/tests/test_job.py +++ b/lint-workflow/tests/test_job.py @@ -1,11 +1,8 @@ """Test src/models/job.py.""" import pytest -from .context import src - from src.models.job import Job from src.models.step import Step -from src.models.workflow import Workflow @pytest.fixture(name="default_job_data") diff --git a/lint-workflow/tests/test_lint.py b/lint-workflow/tests/test_lint.py index eee5bddf..d9a28e37 100644 --- a/lint-workflow/tests/test_lint.py +++ b/lint-workflow/tests/test_lint.py @@ -1,15 +1,12 @@ -import json +"""Test src/lint.py.""" import pytest -from .conftest import FIXTURE_DIR -from .context import src - from src.lint import LinterCmd from src.utils import Settings, LintFinding, LintLevels -@pytest.fixture -def settings(): +@pytest.fixture(name="settings") +def fixture_settings(): return Settings() diff --git a/lint-workflow/tests/test_rule.py b/lint-workflow/tests/test_rule.py index 176ad19a..9eb348e2 100644 --- a/lint-workflow/tests/test_rule.py +++ b/lint-workflow/tests/test_rule.py @@ -5,8 +5,10 @@ from ruamel.yaml import YAML from src.load import WorkflowBuilder +from src.models.job import Job +from src.models.step import Step +from src.models.workflow import Workflow from src.rule import Rule, RuleExecutionException -from src.models import Workflow, Job, Step yaml = YAML() diff --git a/lint-workflow/tests/test_step.py b/lint-workflow/tests/test_step.py index 485c7b16..9acd2e81 100644 --- a/lint-workflow/tests/test_step.py +++ b/lint-workflow/tests/test_step.py @@ -7,7 +7,6 @@ from src.models.step import Step - @pytest.fixture(name="default_step") def fixture_default_step(): step_str = """\ From 5e4c9801baa935238412041231c326299be6eda1 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 12 Jan 2024 12:25:10 -0800 Subject: [PATCH 31/57] Reformat --- lint-workflow/src/actions.py | 8 +++----- lint-workflow/src/lint.py | 3 ++- lint-workflow/src/load.py | 3 ++- lint-workflow/src/models/job.py | 1 + lint-workflow/src/models/step.py | 1 + lint-workflow/src/models/workflow.py | 1 + lint-workflow/src/rule.py | 1 + lint-workflow/src/rules/job_environment_prefix.py | 1 + lint-workflow/src/rules/name_capitalized.py | 1 + lint-workflow/src/rules/name_exists.py | 1 + lint-workflow/src/rules/pinned_job_runner.py | 1 + lint-workflow/src/rules/step_approved.py | 1 + lint-workflow/src/rules/step_pinned.py | 1 + lint-workflow/src/utils.py | 1 + 14 files changed, 18 insertions(+), 7 deletions(-) diff --git a/lint-workflow/src/actions.py b/lint-workflow/src/actions.py index eff1bba0..832af6eb 100644 --- a/lint-workflow/src/actions.py +++ b/lint-workflow/src/actions.py @@ -54,9 +54,7 @@ def extend_parser(subparsers: argparse.ArgumentParser) -> argparse.ArgumentParse subparsers_actions = parser_actions.add_subparsers( required=True, dest="actions_command" ) - subparsers_actions.add_parser( - "update", help="update action versions" - ) + subparsers_actions.add_parser("update", help="update action versions") parser_actions_add = subparsers_actions.add_parser( "add", help="add action to approved list" ) @@ -80,7 +78,7 @@ def get_github_api_response( if response.status == 403 and response.reason == "rate limit exceeded": logging.error( "Failed to call GitHub API for action: %s due to rate limit exceeded.", - action_name + action_name, ) return None @@ -88,7 +86,7 @@ def get_github_api_response( logging.error( "Failed to call GitHub API for action: %s: %s.", action_name, - response.data + response.data, ) return None diff --git a/lint-workflow/src/lint.py b/lint-workflow/src/lint.py index 97145b7d..52e76e51 100644 --- a/lint-workflow/src/lint.py +++ b/lint-workflow/src/lint.py @@ -16,7 +16,8 @@ class LinterCmd: Supporting logic is supplied to: - build out the list of Rules desired - select and validate the workflow files to lint - """ + """ + def __init__(self, settings: Settings = None) -> None: """Initailized the LinterCmd class. diff --git a/lint-workflow/src/load.py b/lint-workflow/src/load.py index 2bdc902f..dfd9f595 100644 --- a/lint-workflow/src/load.py +++ b/lint-workflow/src/load.py @@ -111,12 +111,13 @@ class Rules: workflow. It also assists in making sure the Rules that apply to multiple types are not skipped. """ + workflow: List[Rule] = [] job: List[Rule] = [] step: List[Rule] = [] def __init__(self, settings: Settings) -> None: - """Initializes the Rules + """Initializes the Rules Args: settings: diff --git a/lint-workflow/src/models/job.py b/lint-workflow/src/models/job.py index 6f5ccbca..c841c443 100644 --- a/lint-workflow/src/models/job.py +++ b/lint-workflow/src/models/job.py @@ -17,6 +17,7 @@ class Job: Rules against. If a new Rule requies a key that is missing, the attribute should be added to this class to make it available for use in linting. """ + runs_on: str = field(metadata=config(field_name="runs-on")) key: Optional[str] = None name: Optional[str] = None diff --git a/lint-workflow/src/models/step.py b/lint-workflow/src/models/step.py index ceabd7b5..0e7c3aa1 100644 --- a/lint-workflow/src/models/step.py +++ b/lint-workflow/src/models/step.py @@ -15,6 +15,7 @@ class Step: Rules against. If a new Rule requies a key that is missing, the attribute should be added to this class to make it available for use in linting. """ + key: Optional[int] = None job: Optional[str] = None name: Optional[str] = None diff --git a/lint-workflow/src/models/workflow.py b/lint-workflow/src/models/workflow.py index f8a52b2d..aef36de8 100644 --- a/lint-workflow/src/models/workflow.py +++ b/lint-workflow/src/models/workflow.py @@ -20,6 +20,7 @@ class Workflow: See src/models/job.py for an example if the key in the workflow data does not map one-to-one in the model (ex. 'with' => 'uses_with') """ + key: str = "" name: Optional[str] = None on: Optional[CommentedMap] = None diff --git a/lint-workflow/src/rule.py b/lint-workflow/src/rule.py index f27ee323..1ad9f63b 100644 --- a/lint-workflow/src/rule.py +++ b/lint-workflow/src/rule.py @@ -9,6 +9,7 @@ class RuleExecutionException(Exception): """Exeception for the Base Rule class.""" + pass diff --git a/lint-workflow/src/rules/job_environment_prefix.py b/lint-workflow/src/rules/job_environment_prefix.py index a864c063..d611d403 100644 --- a/lint-workflow/src/rules/job_environment_prefix.py +++ b/lint-workflow/src/rules/job_environment_prefix.py @@ -22,6 +22,7 @@ class RuleJobEnvironmentPrefix(Rule): variables are normally at the top of the file and Step level ones are pretty visible when debugging a shell Step. """ + def __init__(self, settings: Settings = None) -> None: """RuleJobEnvironmentPrefix constructor to override the Rule class. diff --git a/lint-workflow/src/rules/name_capitalized.py b/lint-workflow/src/rules/name_capitalized.py index 4e24bbfc..c2c50d3b 100644 --- a/lint-workflow/src/rules/name_capitalized.py +++ b/lint-workflow/src/rules/name_capitalized.py @@ -13,6 +13,7 @@ class RuleNameCapitalized(Rule): A simple standard to help keep uniformity in naming. """ + def __init__(self, settings: Settings = None) -> None: """Contructor for RuleNameCapitalized to override the Rule class. diff --git a/lint-workflow/src/rules/name_exists.py b/lint-workflow/src/rules/name_exists.py index 021218d3..f2b78e60 100644 --- a/lint-workflow/src/rules/name_exists.py +++ b/lint-workflow/src/rules/name_exists.py @@ -17,6 +17,7 @@ class RuleNameExists(Rule): It also helps with uniformity of runs. """ + def __init__(self, settings: Settings = None) -> None: """Contructor for RuleNameCapitalized to override Rule class. diff --git a/lint-workflow/src/rules/pinned_job_runner.py b/lint-workflow/src/rules/pinned_job_runner.py index 135baea2..14cefbdf 100644 --- a/lint-workflow/src/rules/pinned_job_runner.py +++ b/lint-workflow/src/rules/pinned_job_runner.py @@ -15,6 +15,7 @@ class RuleJobRunnerVersionPinned(Rule): in the past. To avoid this and prevent a single event from breaking the majority of our pipelines, we pin the versions. """ + def __init__(self, settings: Settings = None) -> None: """Constructor for RuleJobRunnerVersionPinned to override Rule class. diff --git a/lint-workflow/src/rules/step_approved.py b/lint-workflow/src/rules/step_approved.py index 3ada71df..44421643 100644 --- a/lint-workflow/src/rules/step_approved.py +++ b/lint-workflow/src/rules/step_approved.py @@ -15,6 +15,7 @@ class RuleStepUsesApproved(Rule): are required to pass a security review and be added to the pre-approved list to check against. """ + def __init__(self, settings: Settings = None) -> None: """Constructor for RuleStepUsesApproved to override Rule class. diff --git a/lint-workflow/src/rules/step_pinned.py b/lint-workflow/src/rules/step_pinned.py index cdbfbf82..9b2d7a44 100644 --- a/lint-workflow/src/rules/step_pinned.py +++ b/lint-workflow/src/rules/step_pinned.py @@ -23,6 +23,7 @@ class RuleStepUsesPinned(Rule): spamming a bunch of PRs across all of our repos when `bitwarden/gh-actions` is updated. """ + def __init__(self, settings: Settings = None) -> None: """Constructor for RuleStepUsesPinned to override base Rule. diff --git a/lint-workflow/src/utils.py b/lint-workflow/src/utils.py index 1c762958..8ae91676 100644 --- a/lint-workflow/src/utils.py +++ b/lint-workflow/src/utils.py @@ -100,6 +100,7 @@ class SettingsError(Exception): class Settings: """Class that contains configuration-as-code for any portion of the app.""" + enabled_rules: list[str] approved_actions: dict[str, Action] From e49cde4aac089cbb9bf9fbc6f1972d717d00b4d6 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 12 Jan 2024 15:37:58 -0800 Subject: [PATCH 32/57] Fix type hinting --- lint-workflow/src/actions.py | 11 ++++++----- lint-workflow/src/lint.py | 5 +++-- lint-workflow/src/load.py | 7 +++++-- lint-workflow/src/models/job.py | 4 ++-- lint-workflow/src/models/step.py | 2 +- lint-workflow/src/rule.py | 6 +++--- lint-workflow/src/rules/job_environment_prefix.py | 12 ++++++------ lint-workflow/src/rules/name_capitalized.py | 8 ++++---- lint-workflow/src/rules/name_exists.py | 8 ++++---- lint-workflow/src/rules/pinned_job_runner.py | 10 +++++----- lint-workflow/src/rules/step_approved.py | 12 ++++++------ lint-workflow/src/rules/step_pinned.py | 12 ++++++------ lint-workflow/src/utils.py | 10 ++++++---- 13 files changed, 57 insertions(+), 50 deletions(-) diff --git a/lint-workflow/src/actions.py b/lint-workflow/src/actions.py index 832af6eb..715c5ea5 100644 --- a/lint-workflow/src/actions.py +++ b/lint-workflow/src/actions.py @@ -6,7 +6,7 @@ import urllib3 as urllib from dataclasses import asdict -from typing import Union, Tuple +from typing import Optional, Tuple, Union from src.utils import Colors, Settings, Action @@ -24,7 +24,7 @@ class ActionsCmd: """ - def __init__(self, settings: Settings = None) -> None: + def __init__(self, settings: Optional[Settings] = None) -> None: """Initialize the the ActionsCmd class. Args: @@ -35,7 +35,7 @@ def __init__(self, settings: Settings = None) -> None: self.settings = settings @staticmethod - def extend_parser(subparsers: argparse.ArgumentParser) -> argparse.ArgumentParser: + def extend_parser(subparsers: argparse._SubParsersAction) -> argparse._SubParsersAction: """Extends the CLI subparser with the options for ActionCmd. Add 'actions add' and 'actions update' to the CLI as sub-commands @@ -109,7 +109,7 @@ def exists(self, action: Action) -> bool: return True - def get_latest_version(self, action: Action) -> Tuple[str, str]: + def get_latest_version(self, action: Action) -> Action | None: """Gets the latest version of the Action to compare against.""" # Get tag from latest release @@ -168,7 +168,8 @@ def add(self, new_action_name: str, filename: str) -> None: if self.exists(proposed_action): latest = self.get_latest_version(proposed_action) - updated_actions[latest.name] = latest + if latest: + updated_actions[latest.name] = latest self.save_actions(updated_actions, filename) diff --git a/lint-workflow/src/lint.py b/lint-workflow/src/lint.py index 52e76e51..c87cea2f 100644 --- a/lint-workflow/src/lint.py +++ b/lint-workflow/src/lint.py @@ -4,6 +4,7 @@ import os from functools import reduce +from typing import Optional from src.load import WorkflowBuilder, Rules from src.utils import LintFinding, Settings @@ -18,7 +19,7 @@ class LinterCmd: - select and validate the workflow files to lint """ - def __init__(self, settings: Settings = None) -> None: + def __init__(self, settings: Optional[Settings] = None) -> None: """Initailized the LinterCmd class. Args: @@ -29,7 +30,7 @@ def __init__(self, settings: Settings = None) -> None: self.rules = Rules(settings=settings) @staticmethod - def extend_parser(subparsers: argparse.ArgumentParser) -> argparse.ArgumentParser: + def extend_parser(subparsers: argparse._SubParsersAction) -> argparse._SubParsersAction: """Extends the CLI subparser with the options for LintCmd. Add 'lint' as a sub command along with its options and arguments diff --git a/lint-workflow/src/load.py b/lint-workflow/src/load.py index dfd9f595..ecd26a17 100644 --- a/lint-workflow/src/load.py +++ b/lint-workflow/src/load.py @@ -1,6 +1,6 @@ """Module to load for Worflows and Rules.""" import importlib -from typing import List +from typing import List, Optional from ruamel.yaml import YAML from ruamel.yaml.comments import CommentedMap @@ -72,7 +72,10 @@ def __build_workflow(cls, loaded_yaml: CommentedMap) -> Workflow: @classmethod def build( - cls, filename: str = None, workflow: CommentedMap = None, from_file: bool = True + cls, + filename: Optional[str] = None, + workflow: Optional[CommentedMap] = None, + from_file: bool = True ) -> Workflow: """Build a Workflow from either code or a file. diff --git a/lint-workflow/src/models/job.py b/lint-workflow/src/models/job.py index c841c443..f01913c0 100644 --- a/lint-workflow/src/models/job.py +++ b/lint-workflow/src/models/job.py @@ -22,10 +22,10 @@ class Job: key: Optional[str] = None name: Optional[str] = None env: Optional[CommentedMap] = None - steps: List[Step] = None + steps: Optional[List[Step]] = None @classmethod - def init(cls, key: str, data: CommentedMap) -> Self: + def init(cls: Self, key: str, data: CommentedMap) -> Self: """Custom dataclass constructor to map job data to a Job.""" new_job = cls.from_dict(data) new_job.key = key diff --git a/lint-workflow/src/models/step.py b/lint-workflow/src/models/step.py index 0e7c3aa1..a97ce917 100644 --- a/lint-workflow/src/models/step.py +++ b/lint-workflow/src/models/step.py @@ -31,7 +31,7 @@ class Step: run: Optional[str] = None @classmethod - def init(cls, idx: int, job: str, data: CommentedMap) -> Self: + def init(cls: Self, idx: int, job: str, data: CommentedMap) -> Self: """Custom dataclass constructor to map a job step data to a Step.""" new_step = cls.from_dict(data) diff --git a/lint-workflow/src/rule.py b/lint-workflow/src/rule.py index 1ad9f63b..59a79f7a 100644 --- a/lint-workflow/src/rule.py +++ b/lint-workflow/src/rule.py @@ -1,5 +1,5 @@ """Base Rule class to build rules by extending.""" -from typing import Union, List +from typing import List, Optional, Tuple, Union from .models.workflow import Workflow from .models.job import Job @@ -18,9 +18,9 @@ class Rule: on_fail: LintLevels = LintLevels.ERROR compatibility: List[Union[Workflow, Job, Step]] = [Workflow, Job, Step] - settings: Settings = None + settings: Optional[Settings] = None - def fn(self, obj: Union[Workflow, Job, Step]) -> bool: + def fn(self, obj: Union[Workflow, Job, Step]) -> Tuple[bool, str]: """Execute the Rule (this should be overriden in the extending class. Args: diff --git a/lint-workflow/src/rules/job_environment_prefix.py b/lint-workflow/src/rules/job_environment_prefix.py index d611d403..5f81e50b 100644 --- a/lint-workflow/src/rules/job_environment_prefix.py +++ b/lint-workflow/src/rules/job_environment_prefix.py @@ -1,5 +1,5 @@ """A Rule to enforce prefixes environment variables.""" -from typing import Union, Tuple, List +from typing import Union, Optional, Tuple, List from ..models.job import Job from ..models.workflow import Workflow @@ -23,7 +23,7 @@ class RuleJobEnvironmentPrefix(Rule): visible when debugging a shell Step. """ - def __init__(self, settings: Settings = None) -> None: + def __init__(self, settings: Optional[Settings] = None) -> None: """RuleJobEnvironmentPrefix constructor to override the Rule class. Args: @@ -31,10 +31,10 @@ def __init__(self, settings: Settings = None) -> None: A Settings object that contains any default, overriden, or custom settings required anywhere in the application. """ - self.message: str = "Job environment vars should start with and underscore:" - self.on_fail: LintLevels = LintLevels.ERROR - self.compatibility: List[Union[Workflow, Job, Step]] = [Job] - self.settings: Settings = settings + self.message = "Job environment vars should start with and underscore:" + self.on_fail = LintLevels.ERROR + self.compatibility = [Job] + self.settings = settings def fn(self, obj: Job) -> Tuple[bool, str]: """Enforces the underscore prefix standard on job envs. diff --git a/lint-workflow/src/rules/name_capitalized.py b/lint-workflow/src/rules/name_capitalized.py index c2c50d3b..340afa91 100644 --- a/lint-workflow/src/rules/name_capitalized.py +++ b/lint-workflow/src/rules/name_capitalized.py @@ -1,5 +1,5 @@ """A Rule to enforce all 'name' values start with a capital letter.""" -from typing import Tuple, Union +from typing import Optional, Tuple, Union from ..models.workflow import Workflow from ..models.job import Job @@ -14,7 +14,7 @@ class RuleNameCapitalized(Rule): A simple standard to help keep uniformity in naming. """ - def __init__(self, settings: Settings = None) -> None: + def __init__(self, settings: Optional[Settings] = None) -> None: """Contructor for RuleNameCapitalized to override the Rule class. Args: @@ -23,8 +23,8 @@ def __init__(self, settings: Settings = None) -> None: required anywhere in the application. """ self.message = "name must capitalized" - self.on_fail: LintLevels = LintLevels.ERROR - self.settings: Settings = settings + self.on_fail = LintLevels.ERROR + self.settings = settings def fn(self, obj: Union[Workflow, Job, Step]) -> Tuple[bool, str]: """Enforces capitalization of the first letter of any name key. diff --git a/lint-workflow/src/rules/name_exists.py b/lint-workflow/src/rules/name_exists.py index f2b78e60..73f204d8 100644 --- a/lint-workflow/src/rules/name_exists.py +++ b/lint-workflow/src/rules/name_exists.py @@ -1,5 +1,5 @@ """A Rule to enforce that a 'name' exists.""" -from typing import Tuple, Union +from typing import Optional, Tuple, Union from ..models.workflow import Workflow from ..models.job import Job @@ -18,7 +18,7 @@ class RuleNameExists(Rule): It also helps with uniformity of runs. """ - def __init__(self, settings: Settings = None) -> None: + def __init__(self, settings: Optional[Settings] = None) -> None: """Contructor for RuleNameCapitalized to override Rule class. Args: @@ -27,8 +27,8 @@ def __init__(self, settings: Settings = None) -> None: required anywhere in the application. """ self.message = "name must exist" - self.on_fail: LintLevels = LintLevels.ERROR - self.settings: Settings = settings + self.on_fail = LintLevels.ERROR + self.settings = settings def fn(self, obj: Union[Workflow, Job, Step]) -> Tuple[bool, str]: """Enforces the existence of names. diff --git a/lint-workflow/src/rules/pinned_job_runner.py b/lint-workflow/src/rules/pinned_job_runner.py index 14cefbdf..6397c3ea 100644 --- a/lint-workflow/src/rules/pinned_job_runner.py +++ b/lint-workflow/src/rules/pinned_job_runner.py @@ -1,5 +1,5 @@ """A Rule to enforce pinning runners to a specific OS version.""" -from typing import List, Tuple, Union +from typing import List, Optional, Tuple, Union from ..models.job import Job from ..models.workflow import Workflow @@ -16,7 +16,7 @@ class RuleJobRunnerVersionPinned(Rule): our pipelines, we pin the versions. """ - def __init__(self, settings: Settings = None) -> None: + def __init__(self, settings: Optional[Settings] = None) -> None: """Constructor for RuleJobRunnerVersionPinned to override Rule class. Args: @@ -25,9 +25,9 @@ def __init__(self, settings: Settings = None) -> None: required anywhere in the application. """ self.message = "Workflow runner must be pinned" - self.on_fail: LintLevels = LintLevels.ERROR - self.compatibility: List[Union[Workflow, Job, Step]] = [Job] - self.settings: Settings = settings + self.on_fail = LintLevels.ERROR + self.compatibility = [Job] + self.settings = settings def fn(self, obj: Job) -> Tuple[bool, str]: """Enforces runners are pinned to a version diff --git a/lint-workflow/src/rules/step_approved.py b/lint-workflow/src/rules/step_approved.py index 44421643..06ab20b1 100644 --- a/lint-workflow/src/rules/step_approved.py +++ b/lint-workflow/src/rules/step_approved.py @@ -1,5 +1,5 @@ """A Rule to enforce the use of a list of pre-approved Actions.""" -from typing import List, Tuple, Union +from typing import List, Optional, Tuple, Union from ..models.job import Job from ..models.workflow import Workflow @@ -16,7 +16,7 @@ class RuleStepUsesApproved(Rule): check against. """ - def __init__(self, settings: Settings = None) -> None: + def __init__(self, settings: Optional[Settings] = None) -> None: """Constructor for RuleStepUsesApproved to override Rule class. Args: @@ -24,9 +24,9 @@ def __init__(self, settings: Settings = None) -> None: A Settings object that contains any default, overriden, or custom settings required anywhere in the application. """ - self.on_fail: LintLevels = LintLevels.WARNING - self.compatibility: List[Union[Workflow, Job, Step]] = [Step] - self.settings: Settings = settings + self.on_fail = LintLevels.WARNING + self.compatibility = [Step] + self.settings = settings def skip(self, obj: Step) -> bool: """Skip this Rule on some Steps. @@ -36,7 +36,7 @@ def skip(self, obj: Step) -> bool: """ ## Force pass for any shell steps if not obj.uses: - return True, "" + return True ## Force pass for any local actions if "@" not in obj.uses: diff --git a/lint-workflow/src/rules/step_pinned.py b/lint-workflow/src/rules/step_pinned.py index 9b2d7a44..8aa6c3ca 100644 --- a/lint-workflow/src/rules/step_pinned.py +++ b/lint-workflow/src/rules/step_pinned.py @@ -1,5 +1,5 @@ """A Rule to enforce Actions are pinned correctly.""" -from typing import List, Tuple, Union +from typing import List, Optional, Tuple, Union from ..models.job import Job from ..models.workflow import Workflow @@ -24,7 +24,7 @@ class RuleStepUsesPinned(Rule): updated. """ - def __init__(self, settings: Settings = None) -> None: + def __init__(self, settings: Optional[Settings] = None) -> None: """Constructor for RuleStepUsesPinned to override base Rule. Args: @@ -32,9 +32,9 @@ def __init__(self, settings: Settings = None) -> None: A Settings object that contains any default, overriden, or custom settings required anywhere in the application. """ - self.on_fail: LintLevels = LintLevels.ERROR - self.compatibility: List[Union[Workflow, Job, Step]] = [Step] - self.settings: Settings = settings + self.on_fail = LintLevels.ERROR + self.compatibility = [Step] + self.settings = settings def skip(self, obj: Step) -> bool: """Skip this Rule on some Steps. @@ -43,7 +43,7 @@ def skip(self, obj: Step) -> bool: Rules are skipped. """ if not obj.uses: - return True, "" + return True ## Force pass for any local actions if "@" not in obj.uses: diff --git a/lint-workflow/src/utils.py b/lint-workflow/src/utils.py index 8ae91676..e26046cc 100644 --- a/lint-workflow/src/utils.py +++ b/lint-workflow/src/utils.py @@ -1,7 +1,7 @@ """Module of a collection of random utilities.""" from dataclasses import dataclass from enum import Enum -from typing import Self +from typing import Optional, Self @dataclass @@ -38,7 +38,9 @@ class LintFinding: """Represents a problem detected by linting.""" def __init__( - self, description: str = "", level: LintLevel = None + self, + description: str, + level: LintLevels ) -> None: self.description = description self.level = level @@ -106,8 +108,8 @@ class Settings: def __init__( self, - enabled_rules: list[str] = None, - approved_actions: dict[str, dict[str, str]] = None, + enabled_rules: Optional[list[str]] = None, + approved_actions: Optional[dict[str, dict[str, str]]] = None, ): """Settings object that can be overriden in settings.py. From 4ca8b814e266b6d7a10c7b3963b5301748b4fccf Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 12 Jan 2024 15:41:58 -0800 Subject: [PATCH 33/57] Move lint-workflow to vs directory restore lint-workflow --- .github/workflows/lint-ci.yml | 26 +- .../.gitignore | 0 lint-workflow-v2/.yamllint.yml | 36 + lint-workflow-v2/Pipfile | 22 + lint-workflow-v2/Pipfile.lock | 814 ++++++++++++++++++ lint-workflow-v2/README.md | 123 +++ .../Taskfile.yml | 0 lint-workflow-v2/action.yml | 32 + .../actions.json | 0 {lint-workflow => lint-workflow-v2}/cli.py | 0 .../flake.lock | 0 {lint-workflow => lint-workflow-v2}/flake.nix | 0 {lint-workflow => lint-workflow-v2}/pylintrc | 0 .../settings.py | 0 .../src/__init__.py | 0 .../src/actions.py | 0 .../src/lint.py | 0 .../src/load.py | 0 .../src/models/__init__.py | 0 .../src/models/job.py | 0 .../src/models/step.py | 0 .../src/models/workflow.py | 0 .../src/rule.py | 0 .../src/rules/__init__.py | 0 .../src/rules/job_environment_prefix.py | 0 .../src/rules/name_capitalized.py | 0 .../src/rules/name_exists.py | 0 .../src/rules/pinned_job_runner.py | 0 .../src/rules/step_approved.py | 0 .../src/rules/step_pinned.py | 0 .../src/utils.py | 0 .../tests}/__init__.py | 0 .../tests/conftest.py | 0 lint-workflow-v2/tests/fixtures/test-alt.yml | 25 + .../tests/fixtures/test-min-incorrect.yaml | 0 .../tests/fixtures/test-min.yaml | 0 lint-workflow-v2/tests/fixtures/test.yml | 55 ++ lint-workflow-v2/tests/fixtures/test_a.yaml | 27 + lint-workflow-v2/tests/rules/__init__.py | 0 .../rules/test_job_environment_prefix.py | 0 .../tests/rules/test_name_capitalized.py | 0 .../tests/rules/test_name_exists.py | 0 .../tests/rules/test_pinned_job_runner.py | 0 .../tests/rules/test_step_approved.py | 0 .../tests/rules/test_step_pinned.py | 0 .../tests/test_job.py | 0 lint-workflow-v2/tests/test_lint.py | 43 + .../tests/test_load.py | 0 .../tests/test_rule.py | 0 .../tests/test_step.py | 0 .../tests/test_utils.py | 0 .../tests/test_workflow.py | 0 lint-workflow/Pipfile | 9 +- lint-workflow/Pipfile.lock | 710 ++------------- lint-workflow/README.md | 99 +-- lint-workflow/lint.py | 439 ++++++++++ lint-workflow/tests/configs.py | 1 + lint-workflow/tests/fixtures/test.yml | 5 +- lint-workflow/tests/fixtures/test_a.yaml | 2 +- lint-workflow/tests/test_action_update.py | 18 + lint-workflow/tests/test_lint.py | 47 +- lint-workflow/tests/test_main.py | 59 ++ lint-workflow/tests/test_workflow_files.py | 35 + 63 files changed, 1822 insertions(+), 805 deletions(-) rename {lint-workflow => lint-workflow-v2}/.gitignore (100%) create mode 100644 lint-workflow-v2/.yamllint.yml create mode 100644 lint-workflow-v2/Pipfile create mode 100644 lint-workflow-v2/Pipfile.lock create mode 100644 lint-workflow-v2/README.md rename {lint-workflow => lint-workflow-v2}/Taskfile.yml (100%) create mode 100644 lint-workflow-v2/action.yml rename {lint-workflow => lint-workflow-v2}/actions.json (100%) rename {lint-workflow => lint-workflow-v2}/cli.py (100%) rename {lint-workflow => lint-workflow-v2}/flake.lock (100%) rename {lint-workflow => lint-workflow-v2}/flake.nix (100%) rename {lint-workflow => lint-workflow-v2}/pylintrc (100%) rename {lint-workflow => lint-workflow-v2}/settings.py (100%) rename {lint-workflow => lint-workflow-v2}/src/__init__.py (100%) rename {lint-workflow => lint-workflow-v2}/src/actions.py (100%) rename {lint-workflow => lint-workflow-v2}/src/lint.py (100%) rename {lint-workflow => lint-workflow-v2}/src/load.py (100%) rename {lint-workflow => lint-workflow-v2}/src/models/__init__.py (100%) rename {lint-workflow => lint-workflow-v2}/src/models/job.py (100%) rename {lint-workflow => lint-workflow-v2}/src/models/step.py (100%) rename {lint-workflow => lint-workflow-v2}/src/models/workflow.py (100%) rename {lint-workflow => lint-workflow-v2}/src/rule.py (100%) rename {lint-workflow => lint-workflow-v2}/src/rules/__init__.py (100%) rename {lint-workflow => lint-workflow-v2}/src/rules/job_environment_prefix.py (100%) rename {lint-workflow => lint-workflow-v2}/src/rules/name_capitalized.py (100%) rename {lint-workflow => lint-workflow-v2}/src/rules/name_exists.py (100%) rename {lint-workflow => lint-workflow-v2}/src/rules/pinned_job_runner.py (100%) rename {lint-workflow => lint-workflow-v2}/src/rules/step_approved.py (100%) rename {lint-workflow => lint-workflow-v2}/src/rules/step_pinned.py (100%) rename {lint-workflow => lint-workflow-v2}/src/utils.py (100%) rename {lint-workflow/tests/rules => lint-workflow-v2/tests}/__init__.py (100%) rename {lint-workflow => lint-workflow-v2}/tests/conftest.py (100%) create mode 100644 lint-workflow-v2/tests/fixtures/test-alt.yml rename {lint-workflow => lint-workflow-v2}/tests/fixtures/test-min-incorrect.yaml (100%) rename {lint-workflow => lint-workflow-v2}/tests/fixtures/test-min.yaml (100%) create mode 100644 lint-workflow-v2/tests/fixtures/test.yml create mode 100644 lint-workflow-v2/tests/fixtures/test_a.yaml create mode 100644 lint-workflow-v2/tests/rules/__init__.py rename {lint-workflow => lint-workflow-v2}/tests/rules/test_job_environment_prefix.py (100%) rename {lint-workflow => lint-workflow-v2}/tests/rules/test_name_capitalized.py (100%) rename {lint-workflow => lint-workflow-v2}/tests/rules/test_name_exists.py (100%) rename {lint-workflow => lint-workflow-v2}/tests/rules/test_pinned_job_runner.py (100%) rename {lint-workflow => lint-workflow-v2}/tests/rules/test_step_approved.py (100%) rename {lint-workflow => lint-workflow-v2}/tests/rules/test_step_pinned.py (100%) rename {lint-workflow => lint-workflow-v2}/tests/test_job.py (100%) create mode 100644 lint-workflow-v2/tests/test_lint.py rename {lint-workflow => lint-workflow-v2}/tests/test_load.py (100%) rename {lint-workflow => lint-workflow-v2}/tests/test_rule.py (100%) rename {lint-workflow => lint-workflow-v2}/tests/test_step.py (100%) rename {lint-workflow => lint-workflow-v2}/tests/test_utils.py (100%) rename {lint-workflow => lint-workflow-v2}/tests/test_workflow.py (100%) create mode 100644 lint-workflow/lint.py create mode 100644 lint-workflow/tests/configs.py create mode 100644 lint-workflow/tests/test_action_update.py create mode 100644 lint-workflow/tests/test_main.py create mode 100644 lint-workflow/tests/test_workflow_files.py diff --git a/.github/workflows/lint-ci.yml b/.github/workflows/lint-ci.yml index d82c394c..cbc78ed5 100644 --- a/.github/workflows/lint-ci.yml +++ b/.github/workflows/lint-ci.yml @@ -9,7 +9,7 @@ on: workflow_dispatch: {} jobs: - CI: + ci-lint: name: CI runs-on: ubuntu-22.04 steps: @@ -30,4 +30,28 @@ jobs: - name: Test lint working-directory: lint-workflow + run: pipenv run pytest tests + + + ci-lint-v2: + name: CI + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Set up Python + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + with: + python-version: "3.11" + + - name: Install dependencies + working-directory: lint-workflow-v2 + run: | + python -m pip install --upgrade pip + pip install pipenv + pipenv install --dev + + - name: Test lint + working-directory: lint-workflow-v2 run: pipenv run pytest tests --cov=src diff --git a/lint-workflow/.gitignore b/lint-workflow-v2/.gitignore similarity index 100% rename from lint-workflow/.gitignore rename to lint-workflow-v2/.gitignore diff --git a/lint-workflow-v2/.yamllint.yml b/lint-workflow-v2/.yamllint.yml new file mode 100644 index 00000000..bed41fc5 --- /dev/null +++ b/lint-workflow-v2/.yamllint.yml @@ -0,0 +1,36 @@ +--- + +extends: default + +rules: + braces: + level: warning + brackets: + level: warning + colons: + level: warning + commas: + level: warning + comments: + min-spaces-from-content: 1 + empty-lines: + level: warning + hyphens: + level: warning + indentation: + level: warning + spaces: 2 + key-duplicates: + level: warning + line-length: + level: warning + max: 120 + new-line-at-end-of-file: + level: warning + new-lines: + level: warning + trailing-spaces: + level: warning + truthy: + check-keys: false + level: warning diff --git a/lint-workflow-v2/Pipfile b/lint-workflow-v2/Pipfile new file mode 100644 index 00000000..8f75f162 --- /dev/null +++ b/lint-workflow-v2/Pipfile @@ -0,0 +1,22 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +pyyaml = "*" +urllib3 = "*" +pydantic = "*" +"ruamel.yaml" = "*" +dataclasses-json = "*" + +[dev-packages] +black = "*" +pytest = "*" +coverage = "*" +pytest-cov = "*" +pylint = "*" +pytype = "*" + +[requires] +python_version = "3.11" diff --git a/lint-workflow-v2/Pipfile.lock b/lint-workflow-v2/Pipfile.lock new file mode 100644 index 00000000..d891658d --- /dev/null +++ b/lint-workflow-v2/Pipfile.lock @@ -0,0 +1,814 @@ +{ + "_meta": { + "hash": { + "sha256": "8b34ae1118eb5c9fac2c552c644f66f9dbd05415e08adf78efb99ba8a7f072f3" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.11" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "annotated-types": { + "hashes": [ + "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43", + "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d" + ], + "markers": "python_version >= '3.8'", + "version": "==0.6.0" + }, + "dataclasses-json": { + "hashes": [ + "sha256:35cb40aae824736fdf959801356641836365219cfe14caeb115c39136f775d2a", + "sha256:4aeb343357997396f6bca1acae64e486c3a723d8f5c76301888abeccf0c45176" + ], + "index": "pypi", + "version": "==0.6.3" + }, + "marshmallow": { + "hashes": [ + "sha256:4c1daff273513dc5eb24b219a8035559dc573c8f322558ef85f5438ddd1236dd", + "sha256:c21d4b98fee747c130e6bc8f45c4b3199ea66bc00c12ee1f639f0aeca034d5e9" + ], + "markers": "python_version >= '3.8'", + "version": "==3.20.2" + }, + "mypy-extensions": { + "hashes": [ + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.0" + }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2" + }, + "pydantic": { + "hashes": [ + "sha256:b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a", + "sha256:d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4" + ], + "index": "pypi", + "version": "==2.5.3" + }, + "pydantic-core": { + "hashes": [ + "sha256:00646784f6cd993b1e1c0e7b0fdcbccc375d539db95555477771c27555e3c556", + "sha256:00b1087dabcee0b0ffd104f9f53d7d3eaddfaa314cdd6726143af6bc713aa27e", + "sha256:0348b1dc6b76041516e8a854ff95b21c55f5a411c3297d2ca52f5528e49d8411", + "sha256:036137b5ad0cb0004c75b579445a1efccd072387a36c7f217bb8efd1afbe5245", + "sha256:095b707bb287bfd534044166ab767bec70a9bba3175dcdc3371782175c14e43c", + "sha256:0c08de15d50fa190d577e8591f0329a643eeaed696d7771760295998aca6bc66", + "sha256:1302a54f87b5cd8528e4d6d1bf2133b6aa7c6122ff8e9dc5220fbc1e07bffebd", + "sha256:172de779e2a153d36ee690dbc49c6db568d7b33b18dc56b69a7514aecbcf380d", + "sha256:1b027c86c66b8627eb90e57aee1f526df77dc6d8b354ec498be9a757d513b92b", + "sha256:1ce830e480f6774608dedfd4a90c42aac4a7af0a711f1b52f807130c2e434c06", + "sha256:1fd0c1d395372843fba13a51c28e3bb9d59bd7aebfeb17358ffaaa1e4dbbe948", + "sha256:23598acb8ccaa3d1d875ef3b35cb6376535095e9405d91a3d57a8c7db5d29341", + "sha256:24368e31be2c88bd69340fbfe741b405302993242ccb476c5c3ff48aeee1afe0", + "sha256:26a92ae76f75d1915806b77cf459811e772d8f71fd1e4339c99750f0e7f6324f", + "sha256:27e524624eace5c59af499cd97dc18bb201dc6a7a2da24bfc66ef151c69a5f2a", + "sha256:2b8719037e570639e6b665a4050add43134d80b687288ba3ade18b22bbb29dd2", + "sha256:2c5bcf3414367e29f83fd66f7de64509a8fd2368b1edf4351e862910727d3e51", + "sha256:2dbe357bc4ddda078f79d2a36fc1dd0494a7f2fad83a0a684465b6f24b46fe80", + "sha256:2f5fa187bde8524b1e37ba894db13aadd64faa884657473b03a019f625cee9a8", + "sha256:2f6ffc6701a0eb28648c845f4945a194dc7ab3c651f535b81793251e1185ac3d", + "sha256:314ccc4264ce7d854941231cf71b592e30d8d368a71e50197c905874feacc8a8", + "sha256:36026d8f99c58d7044413e1b819a67ca0e0b8ebe0f25e775e6c3d1fabb3c38fb", + "sha256:36099c69f6b14fc2c49d7996cbf4f87ec4f0e66d1c74aa05228583225a07b590", + "sha256:36fa402dcdc8ea7f1b0ddcf0df4254cc6b2e08f8cd80e7010d4c4ae6e86b2a87", + "sha256:370ffecb5316ed23b667d99ce4debe53ea664b99cc37bfa2af47bc769056d534", + "sha256:3860c62057acd95cc84044e758e47b18dcd8871a328ebc8ccdefd18b0d26a21b", + "sha256:399ac0891c284fa8eb998bcfa323f2234858f5d2efca3950ae58c8f88830f145", + "sha256:3a0b5db001b98e1c649dd55afa928e75aa4087e587b9524a4992316fa23c9fba", + "sha256:3dcf1978be02153c6a31692d4fbcc2a3f1db9da36039ead23173bc256ee3b91b", + "sha256:4241204e4b36ab5ae466ecec5c4c16527a054c69f99bba20f6f75232a6a534e2", + "sha256:438027a975cc213a47c5d70672e0d29776082155cfae540c4e225716586be75e", + "sha256:43e166ad47ba900f2542a80d83f9fc65fe99eb63ceec4debec160ae729824052", + "sha256:478e9e7b360dfec451daafe286998d4a1eeaecf6d69c427b834ae771cad4b622", + "sha256:4ce8299b481bcb68e5c82002b96e411796b844d72b3e92a3fbedfe8e19813eab", + "sha256:4f86f1f318e56f5cbb282fe61eb84767aee743ebe32c7c0834690ebea50c0a6b", + "sha256:55a23dcd98c858c0db44fc5c04fc7ed81c4b4d33c653a7c45ddaebf6563a2f66", + "sha256:599c87d79cab2a6a2a9df4aefe0455e61e7d2aeede2f8577c1b7c0aec643ee8e", + "sha256:5aa90562bc079c6c290f0512b21768967f9968e4cfea84ea4ff5af5d917016e4", + "sha256:64634ccf9d671c6be242a664a33c4acf12882670b09b3f163cd00a24cffbd74e", + "sha256:667aa2eac9cd0700af1ddb38b7b1ef246d8cf94c85637cbb03d7757ca4c3fdec", + "sha256:6a31d98c0d69776c2576dda4b77b8e0c69ad08e8b539c25c7d0ca0dc19a50d6c", + "sha256:6af4b3f52cc65f8a0bc8b1cd9676f8c21ef3e9132f21fed250f6958bd7223bed", + "sha256:6c8edaea3089bf908dd27da8f5d9e395c5b4dc092dbcce9b65e7156099b4b937", + "sha256:71d72ca5eaaa8d38c8df16b7deb1a2da4f650c41b58bb142f3fb75d5ad4a611f", + "sha256:72f9a942d739f09cd42fffe5dc759928217649f070056f03c70df14f5770acf9", + "sha256:747265448cb57a9f37572a488a57d873fd96bf51e5bb7edb52cfb37124516da4", + "sha256:75ec284328b60a4e91010c1acade0c30584f28a1f345bc8f72fe8b9e46ec6a96", + "sha256:78d0768ee59baa3de0f4adac9e3748b4b1fffc52143caebddfd5ea2961595277", + "sha256:78ee52ecc088c61cce32b2d30a826f929e1708f7b9247dc3b921aec367dc1b23", + "sha256:7be719e4d2ae6c314f72844ba9d69e38dff342bc360379f7c8537c48e23034b7", + "sha256:7e1f4744eea1501404b20b0ac059ff7e3f96a97d3e3f48ce27a139e053bb370b", + "sha256:7e90d6cc4aad2cc1f5e16ed56e46cebf4877c62403a311af20459c15da76fd91", + "sha256:7ebe3416785f65c28f4f9441e916bfc8a54179c8dea73c23023f7086fa601c5d", + "sha256:7f41533d7e3cf9520065f610b41ac1c76bc2161415955fbcead4981b22c7611e", + "sha256:7f5025db12fc6de7bc1104d826d5aee1d172f9ba6ca936bf6474c2148ac336c1", + "sha256:86c963186ca5e50d5c8287b1d1c9d3f8f024cbe343d048c5bd282aec2d8641f2", + "sha256:86ce5fcfc3accf3a07a729779d0b86c5d0309a4764c897d86c11089be61da160", + "sha256:8a14c192c1d724c3acbfb3f10a958c55a2638391319ce8078cb36c02283959b9", + "sha256:8b93785eadaef932e4fe9c6e12ba67beb1b3f1e5495631419c784ab87e975670", + "sha256:8ed1af8692bd8d2a29d702f1a2e6065416d76897d726e45a1775b1444f5928a7", + "sha256:92879bce89f91f4b2416eba4429c7b5ca22c45ef4a499c39f0c5c69257522c7c", + "sha256:94fc0e6621e07d1e91c44e016cc0b189b48db053061cc22d6298a611de8071bb", + "sha256:982487f8931067a32e72d40ab6b47b1628a9c5d344be7f1a4e668fb462d2da42", + "sha256:9862bf828112e19685b76ca499b379338fd4c5c269d897e218b2ae8fcb80139d", + "sha256:99b14dbea2fdb563d8b5a57c9badfcd72083f6006caf8e126b491519c7d64ca8", + "sha256:9c6a5c79b28003543db3ba67d1df336f253a87d3112dac3a51b94f7d48e4c0e1", + "sha256:a19b794f8fe6569472ff77602437ec4430f9b2b9ec7a1105cfd2232f9ba355e6", + "sha256:a306cdd2ad3a7d795d8e617a58c3a2ed0f76c8496fb7621b6cd514eb1532cae8", + "sha256:a3dde6cac75e0b0902778978d3b1646ca9f438654395a362cb21d9ad34b24acf", + "sha256:a874f21f87c485310944b2b2734cd6d318765bcbb7515eead33af9641816506e", + "sha256:a983cca5ed1dd9a35e9e42ebf9f278d344603bfcb174ff99a5815f953925140a", + "sha256:aca48506a9c20f68ee61c87f2008f81f8ee99f8d7f0104bff3c47e2d148f89d9", + "sha256:b2602177668f89b38b9f84b7b3435d0a72511ddef45dc14446811759b82235a1", + "sha256:b3e5fe4538001bb82e2295b8d2a39356a84694c97cb73a566dc36328b9f83b40", + "sha256:b6ca36c12a5120bad343eef193cc0122928c5c7466121da7c20f41160ba00ba2", + "sha256:b89f4477d915ea43b4ceea6756f63f0288941b6443a2b28c69004fe07fde0d0d", + "sha256:b9a9d92f10772d2a181b5ca339dee066ab7d1c9a34ae2421b2a52556e719756f", + "sha256:c99462ffc538717b3e60151dfaf91125f637e801f5ab008f81c402f1dff0cd0f", + "sha256:cb92f9061657287eded380d7dc455bbf115430b3aa4741bdc662d02977e7d0af", + "sha256:cdee837710ef6b56ebd20245b83799fce40b265b3b406e51e8ccc5b85b9099b7", + "sha256:cf10b7d58ae4a1f07fccbf4a0a956d705356fea05fb4c70608bb6fa81d103cda", + "sha256:d15687d7d7f40333bd8266f3814c591c2e2cd263fa2116e314f60d82086e353a", + "sha256:d5c28525c19f5bb1e09511669bb57353d22b94cf8b65f3a8d141c389a55dec95", + "sha256:d5f916acf8afbcab6bacbb376ba7dc61f845367901ecd5e328fc4d4aef2fcab0", + "sha256:dab03ed811ed1c71d700ed08bde8431cf429bbe59e423394f0f4055f1ca0ea60", + "sha256:db453f2da3f59a348f514cfbfeb042393b68720787bbef2b4c6068ea362c8149", + "sha256:de2a0645a923ba57c5527497daf8ec5df69c6eadf869e9cd46e86349146e5975", + "sha256:dea7fcd62915fb150cdc373212141a30037e11b761fbced340e9db3379b892d4", + "sha256:dfcbebdb3c4b6f739a91769aea5ed615023f3c88cb70df812849aef634c25fbe", + "sha256:dfcebb950aa7e667ec226a442722134539e77c575f6cfaa423f24371bb8d2e94", + "sha256:e0641b506486f0b4cd1500a2a65740243e8670a2549bb02bc4556a83af84ae03", + "sha256:e33b0834f1cf779aa839975f9d8755a7c2420510c0fa1e9fa0497de77cd35d2c", + "sha256:e4ace1e220b078c8e48e82c081e35002038657e4b37d403ce940fa679e57113b", + "sha256:e4cf2d5829f6963a5483ec01578ee76d329eb5caf330ecd05b3edd697e7d768a", + "sha256:e574de99d735b3fc8364cba9912c2bec2da78775eba95cbb225ef7dda6acea24", + "sha256:e646c0e282e960345314f42f2cea5e0b5f56938c093541ea6dbf11aec2862391", + "sha256:e8a5ac97ea521d7bde7621d86c30e86b798cdecd985723c4ed737a2aa9e77d0c", + "sha256:eedf97be7bc3dbc8addcef4142f4b4164066df0c6f36397ae4aaed3eb187d8ab", + "sha256:ef633add81832f4b56d3b4c9408b43d530dfca29e68fb1b797dcb861a2c734cd", + "sha256:f27207e8ca3e5e021e2402ba942e5b4c629718e665c81b8b306f3c8b1ddbb786", + "sha256:f85f3843bdb1fe80e8c206fe6eed7a1caeae897e496542cee499c374a85c6e08", + "sha256:f8e81e4b55930e5ffab4a68db1af431629cf2e4066dbdbfef65348b8ab804ea8", + "sha256:f96ae96a060a8072ceff4cfde89d261837b4294a4f28b84a28765470d502ccc6", + "sha256:fd9e98b408384989ea4ab60206b8e100d8687da18b5c813c11e92fd8212a98e0", + "sha256:ffff855100bc066ff2cd3aa4a60bc9534661816b110f0243e59503ec2df38421" + ], + "markers": "python_version >= '3.7'", + "version": "==2.14.6" + }, + "pyyaml": { + "hashes": [ + "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", + "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", + "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", + "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", + "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", + "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", + "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", + "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", + "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", + "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", + "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", + "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", + "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", + "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", + "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", + "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", + "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", + "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", + "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", + "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", + "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", + "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", + "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", + "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", + "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", + "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", + "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", + "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", + "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", + "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", + "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", + "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", + "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", + "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", + "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", + "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", + "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", + "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", + "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", + "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", + "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", + "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", + "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", + "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", + "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", + "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", + "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", + "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", + "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", + "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" + ], + "index": "pypi", + "version": "==6.0.1" + }, + "ruamel.yaml": { + "hashes": [ + "sha256:61917e3a35a569c1133a8f772e1226961bf5a1198bea7e23f06a0841dea1ab0e", + "sha256:a013ac02f99a69cdd6277d9664689eb1acba07069f912823177c5eced21a6ada" + ], + "index": "pypi", + "version": "==0.18.5" + }, + "ruamel.yaml.clib": { + "hashes": [ + "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d", + "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001", + "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462", + "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9", + "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe", + "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b", + "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b", + "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615", + "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62", + "sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15", + "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b", + "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1", + "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9", + "sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675", + "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899", + "sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7", + "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7", + "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312", + "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa", + "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91", + "sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b", + "sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6", + "sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3", + "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334", + "sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5", + "sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3", + "sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe", + "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c", + "sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed", + "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337", + "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880", + "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f", + "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d", + "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248", + "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d", + "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf", + "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512", + "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069", + "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb", + "sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942", + "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d", + "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31", + "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92", + "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5", + "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28", + "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d", + "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1", + "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2", + "sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875", + "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412" + ], + "markers": "python_version < '3.13' and platform_python_implementation == 'CPython'", + "version": "==0.2.8" + }, + "typing-extensions": { + "hashes": [ + "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", + "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + ], + "markers": "python_version >= '3.8'", + "version": "==4.9.0" + }, + "typing-inspect": { + "hashes": [ + "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", + "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78" + ], + "version": "==0.9.0" + }, + "urllib3": { + "hashes": [ + "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", + "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54" + ], + "index": "pypi", + "version": "==2.1.0" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:4a61cf0a59097c7bb52689b0fd63717cd2a8a14dc9f1eee97b82d814881c8c91", + "sha256:d6e62862355f60e716164082d6b4b041d38e2a8cf1c7cd953ded5108bac8ff5c" + ], + "markers": "python_full_version >= '3.8.0'", + "version": "==3.0.2" + }, + "attrs": { + "hashes": [ + "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", + "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2.0" + }, + "black": { + "hashes": [ + "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50", + "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f", + "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e", + "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec", + "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055", + "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3", + "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5", + "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54", + "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b", + "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e", + "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e", + "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba", + "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea", + "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59", + "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d", + "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0", + "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9", + "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a", + "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e", + "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba", + "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2", + "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2" + ], + "index": "pypi", + "version": "==23.12.1" + }, + "click": { + "hashes": [ + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.7" + }, + "coverage": { + "hashes": [ + "sha256:04387a4a6ecb330c1878907ce0dc04078ea72a869263e53c72a1ba5bbdf380ca", + "sha256:0676cd0ba581e514b7f726495ea75aba3eb20899d824636c6f59b0ed2f88c471", + "sha256:0e8d06778e8fbffccfe96331a3946237f87b1e1d359d7fbe8b06b96c95a5407a", + "sha256:0eb3c2f32dabe3a4aaf6441dde94f35687224dfd7eb2a7f47f3fd9428e421058", + "sha256:109f5985182b6b81fe33323ab4707011875198c41964f014579cf82cebf2bb85", + "sha256:13eaf476ec3e883fe3e5fe3707caeb88268a06284484a3daf8250259ef1ba143", + "sha256:164fdcc3246c69a6526a59b744b62e303039a81e42cfbbdc171c91a8cc2f9446", + "sha256:26776ff6c711d9d835557ee453082025d871e30b3fd6c27fcef14733f67f0590", + "sha256:26f66da8695719ccf90e794ed567a1549bb2644a706b41e9f6eae6816b398c4a", + "sha256:29f3abe810930311c0b5d1a7140f6395369c3db1be68345638c33eec07535105", + "sha256:316543f71025a6565677d84bc4df2114e9b6a615aa39fb165d697dba06a54af9", + "sha256:36b0ea8ab20d6a7564e89cb6135920bc9188fb5f1f7152e94e8300b7b189441a", + "sha256:3cc9d4bc55de8003663ec94c2f215d12d42ceea128da8f0f4036235a119c88ac", + "sha256:485e9f897cf4856a65a57c7f6ea3dc0d4e6c076c87311d4bc003f82cfe199d25", + "sha256:5040148f4ec43644702e7b16ca864c5314ccb8ee0751ef617d49aa0e2d6bf4f2", + "sha256:51456e6fa099a8d9d91497202d9563a320513fcf59f33991b0661a4a6f2ad450", + "sha256:53d7d9158ee03956e0eadac38dfa1ec8068431ef8058fe6447043db1fb40d932", + "sha256:5a10a4920def78bbfff4eff8a05c51be03e42f1c3735be42d851f199144897ba", + "sha256:5b14b4f8760006bfdb6e08667af7bc2d8d9bfdb648351915315ea17645347137", + "sha256:5b2ccb7548a0b65974860a78c9ffe1173cfb5877460e5a229238d985565574ae", + "sha256:697d1317e5290a313ef0d369650cfee1a114abb6021fa239ca12b4849ebbd614", + "sha256:6ae8c9d301207e6856865867d762a4b6fd379c714fcc0607a84b92ee63feff70", + "sha256:707c0f58cb1712b8809ece32b68996ee1e609f71bd14615bd8f87a1293cb610e", + "sha256:74775198b702868ec2d058cb92720a3c5a9177296f75bd97317c787daf711505", + "sha256:756ded44f47f330666843b5781be126ab57bb57c22adbb07d83f6b519783b870", + "sha256:76f03940f9973bfaee8cfba70ac991825611b9aac047e5c80d499a44079ec0bc", + "sha256:79287fd95585ed36e83182794a57a46aeae0b64ca53929d1176db56aacc83451", + "sha256:799c8f873794a08cdf216aa5d0531c6a3747793b70c53f70e98259720a6fe2d7", + "sha256:7d360587e64d006402b7116623cebf9d48893329ef035278969fa3bbf75b697e", + "sha256:80b5ee39b7f0131ebec7968baa9b2309eddb35b8403d1869e08f024efd883566", + "sha256:815ac2d0f3398a14286dc2cea223a6f338109f9ecf39a71160cd1628786bc6f5", + "sha256:83c2dda2666fe32332f8e87481eed056c8b4d163fe18ecc690b02802d36a4d26", + "sha256:846f52f46e212affb5bcf131c952fb4075b55aae6b61adc9856222df89cbe3e2", + "sha256:936d38794044b26c99d3dd004d8af0035ac535b92090f7f2bb5aa9c8e2f5cd42", + "sha256:9864463c1c2f9cb3b5db2cf1ff475eed2f0b4285c2aaf4d357b69959941aa555", + "sha256:995ea5c48c4ebfd898eacb098164b3cc826ba273b3049e4a889658548e321b43", + "sha256:a1526d265743fb49363974b7aa8d5899ff64ee07df47dd8d3e37dcc0818f09ed", + "sha256:a56de34db7b7ff77056a37aedded01b2b98b508227d2d0979d373a9b5d353daa", + "sha256:a7c97726520f784239f6c62506bc70e48d01ae71e9da128259d61ca5e9788516", + "sha256:b8e99f06160602bc64da35158bb76c73522a4010f0649be44a4e167ff8555952", + "sha256:bb1de682da0b824411e00a0d4da5a784ec6496b6850fdf8c865c1d68c0e318dd", + "sha256:bf477c355274a72435ceb140dc42de0dc1e1e0bf6e97195be30487d8eaaf1a09", + "sha256:bf635a52fc1ea401baf88843ae8708591aa4adff875e5c23220de43b1ccf575c", + "sha256:bfd5db349d15c08311702611f3dccbef4b4e2ec148fcc636cf8739519b4a5c0f", + "sha256:c530833afc4707fe48524a44844493f36d8727f04dcce91fb978c414a8556cc6", + "sha256:cc6d65b21c219ec2072c1293c505cf36e4e913a3f936d80028993dd73c7906b1", + "sha256:cd3c1e4cb2ff0083758f09be0f77402e1bdf704adb7f89108007300a6da587d0", + "sha256:cfd2a8b6b0d8e66e944d47cdec2f47c48fef2ba2f2dff5a9a75757f64172857e", + "sha256:d0ca5c71a5a1765a0f8f88022c52b6b8be740e512980362f7fdbb03725a0d6b9", + "sha256:e7defbb9737274023e2d7af02cac77043c86ce88a907c58f42b580a97d5bcca9", + "sha256:e9d1bf53c4c8de58d22e0e956a79a5b37f754ed1ffdbf1a260d9dcfa2d8a325e", + "sha256:ea81d8f9691bb53f4fb4db603203029643caffc82bf998ab5b59ca05560f4c06" + ], + "index": "pypi", + "version": "==7.4.0" + }, + "dill": { + "hashes": [ + "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e", + "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03" + ], + "markers": "python_version >= '3.11'", + "version": "==0.3.7" + }, + "importlab": { + "hashes": [ + "sha256:124cfa00e8a34fefe8aac1a5e94f56c781b178c9eb61a1d3f60f7e03b77338d3", + "sha256:b3893853b1f6eb027da509c3b40e6787e95dd66b4b66f1b3613aad77556e1465" + ], + "markers": "python_full_version >= '3.6.0'", + "version": "==0.8.1" + }, + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "isort": { + "hashes": [ + "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", + "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6" + ], + "markers": "python_full_version >= '3.8.0'", + "version": "==5.13.2" + }, + "jinja2": { + "hashes": [ + "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa", + "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.3" + }, + "libcst": { + "hashes": [ + "sha256:003e5e83a12eed23542c4ea20fdc8de830887cc03662432bb36f84f8c4841b81", + "sha256:0acbacb9a170455701845b7e940e2d7b9519db35a86768d86330a0b0deae1086", + "sha256:0bf69cbbab5016d938aac4d3ae70ba9ccb3f90363c588b3b97be434e6ba95403", + "sha256:2d37326bd6f379c64190a28947a586b949de3a76be00176b0732c8ee87d67ebe", + "sha256:3a07ecfabbbb8b93209f952a365549e65e658831e9231649f4f4e4263cad24b1", + "sha256:3ebbb9732ae3cc4ae7a0e97890bed0a57c11d6df28790c2b9c869f7da653c7c7", + "sha256:4bc745d0c06420fe2644c28d6ddccea9474fb68a2135904043676deb4fa1e6bc", + "sha256:5297a16e575be8173185e936b7765c89a3ca69d4ae217a4af161814a0f9745a7", + "sha256:5f1cd308a4c2f71d5e4eec6ee693819933a03b78edb2e4cc5e3ad1afd5fb3f07", + "sha256:63f75656fd733dc20354c46253fde3cf155613e37643c3eaf6f8818e95b7a3d1", + "sha256:73c086705ed34dbad16c62c9adca4249a556c1b022993d511da70ea85feaf669", + "sha256:75816647736f7e09c6120bdbf408456f99b248d6272277eed9a58cf50fb8bc7d", + "sha256:78b7a38ec4c1c009ac39027d51558b52851fb9234669ba5ba62283185963a31c", + "sha256:7ccaf53925f81118aeaadb068a911fac8abaff608817d7343da280616a5ca9c1", + "sha256:82d1271403509b0a4ee6ff7917c2d33b5a015f44d1e208abb1da06ba93b2a378", + "sha256:8ae11eb1ea55a16dc0cdc61b41b29ac347da70fec14cc4381248e141ee2fbe6c", + "sha256:8afb6101b8b3c86c5f9cec6b90ab4da16c3c236fe7396f88e8b93542bb341f7c", + "sha256:8c1f2da45f1c45634090fd8672c15e0159fdc46853336686959b2d093b6e10fa", + "sha256:97fbc73c87e9040e148881041fd5ffa2a6ebf11f64b4ccb5b52e574b95df1a15", + "sha256:99fdc1929703fd9e7408aed2e03f58701c5280b05c8911753a8d8619f7dfdda5", + "sha256:9dffa1795c2804d183efb01c0f1efd20a7831db6a21a0311edf90b4100d67436", + "sha256:bca1841693941fdd18371824bb19a9702d5784cd347cb8231317dbdc7062c5bc", + "sha256:c653d9121d6572d8b7f8abf20f88b0a41aab77ff5a6a36e5a0ec0f19af0072e8", + "sha256:c8f26250f87ca849a7303ed7a4fd6b2c7ac4dec16b7d7e68ca6a476d7c9bfcdb", + "sha256:cc9b6ac36d7ec9db2f053014ea488086ca2ed9c322be104fbe2c71ca759da4bb", + "sha256:d22d1abfe49aa60fc61fa867e10875a9b3024ba5a801112f4d7ba42d8d53242e", + "sha256:d68c34e3038d3d1d6324eb47744cbf13f2c65e1214cf49db6ff2a6603c1cd838", + "sha256:e3d8cf974cfa2487b28f23f56c4bff90d550ef16505e58b0dca0493d5293784b", + "sha256:f36f592e035ef84f312a12b75989dde6a5f6767fe99146cdae6a9ee9aff40dd0", + "sha256:f561c9a84eca18be92f4ad90aa9bd873111efbea995449301719a1a7805dbc5c", + "sha256:fe41b33aa73635b1651f64633f429f7aa21f86d2db5748659a99d9b7b1ed2a90" + ], + "markers": "python_version >= '3.8'", + "version": "==1.1.0" + }, + "markupsafe": { + "hashes": [ + "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e", + "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e", + "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431", + "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686", + "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c", + "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559", + "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc", + "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb", + "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939", + "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c", + "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0", + "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4", + "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9", + "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575", + "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba", + "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d", + "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd", + "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3", + "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00", + "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155", + "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac", + "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52", + "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f", + "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8", + "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b", + "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007", + "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24", + "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea", + "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198", + "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0", + "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee", + "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be", + "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2", + "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1", + "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707", + "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6", + "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c", + "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58", + "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823", + "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779", + "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636", + "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c", + "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad", + "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee", + "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc", + "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2", + "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48", + "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7", + "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e", + "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b", + "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa", + "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5", + "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e", + "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb", + "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9", + "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57", + "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc", + "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc", + "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2", + "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.3" + }, + "mccabe": { + "hashes": [ + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.0" + }, + "mypy-extensions": { + "hashes": [ + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.0" + }, + "networkx": { + "hashes": [ + "sha256:4f33f68cb2afcf86f28a45f43efc27a9386b535d567d2127f8f61d51dec58d36", + "sha256:de346335408f84de0eada6ff9fafafff9bcda11f0a0dfaa931133debb146ab61" + ], + "markers": "python_version >= '3.8'", + "version": "==3.1" + }, + "ninja": { + "hashes": [ + "sha256:18302d96a5467ea98b68e1cae1ae4b4fb2b2a56a82b955193c637557c7273dbd", + "sha256:185e0641bde601e53841525c4196278e9aaf4463758da6dd1e752c0a0f54136a", + "sha256:376889c76d87b95b5719fdd61dd7db193aa7fd4432e5d52d2e44e4c497bdbbee", + "sha256:3e0f9be5bb20d74d58c66cc1c414c3e6aeb45c35b0d0e41e8d739c2c0d57784f", + "sha256:73b93c14046447c7c5cc892433d4fae65d6364bec6685411cb97a8bcf815f93a", + "sha256:7563ce1d9fe6ed5af0b8dd9ab4a214bf4ff1f2f6fd6dc29f480981f0f8b8b249", + "sha256:76482ba746a2618eecf89d5253c0d1e4f1da1270d41e9f54dfbd91831b0f6885", + "sha256:84502ec98f02a037a169c4b0d5d86075eaf6afc55e1879003d6cab51ced2ea4b", + "sha256:95da904130bfa02ea74ff9c0116b4ad266174fafb1c707aa50212bc7859aebf1", + "sha256:9d793b08dd857e38d0b6ffe9e6b7145d7c485a42dcfea04905ca0cdb6017cc3c", + "sha256:9df724344202b83018abb45cb1efc22efd337a1496514e7e6b3b59655be85205", + "sha256:aad34a70ef15b12519946c5633344bc775a7656d789d9ed5fdb0d456383716ef", + "sha256:d491fc8d89cdcb416107c349ad1e3a735d4c4af5e1cb8f5f727baca6350fdaea", + "sha256:ecf80cf5afd09f14dcceff28cb3f11dc90fb97c999c89307aea435889cb66877", + "sha256:fa2ba9d74acfdfbfbcf06fad1b8282de8a7a8c481d9dee45c859a8c93fcc1082" + ], + "version": "==1.11.1.1" + }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2" + }, + "pathspec": { + "hashes": [ + "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", + "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" + ], + "markers": "python_version >= '3.8'", + "version": "==0.12.1" + }, + "platformdirs": { + "hashes": [ + "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380", + "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420" + ], + "markers": "python_version >= '3.8'", + "version": "==4.1.0" + }, + "pluggy": { + "hashes": [ + "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12", + "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7" + ], + "markers": "python_version >= '3.8'", + "version": "==1.3.0" + }, + "pycnite": { + "hashes": [ + "sha256:7d02eb0ec4b405d8812ce053434dacfc2335dcd458ab58a1a8bf64f72d40bd76", + "sha256:ad8616982beecc39f2090999aa8fe0b044b1f6733ec39484cb5e0900b3c88aa1" + ], + "markers": "python_version >= '3.8'", + "version": "==2023.10.11" + }, + "pydot": { + "hashes": [ + "sha256:408a47913ea7bd5d2d34b274144880c1310c4aee901f353cf21fe2e526a4ea28", + "sha256:60246af215123fa062f21cd791be67dda23a6f280df09f68919e637a1e4f3235" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "pylint": { + "hashes": [ + "sha256:58c2398b0301e049609a8429789ec6edf3aabe9b6c5fec916acd18639c16de8b", + "sha256:7a1585285aefc5165db81083c3e06363a27448f6b467b3b0f30dbd0ac1f73810" + ], + "index": "pypi", + "version": "==3.0.3" + }, + "pyparsing": { + "hashes": [ + "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb", + "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db" + ], + "markers": "python_full_version >= '3.6.8'", + "version": "==3.1.1" + }, + "pytest": { + "hashes": [ + "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", + "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8" + ], + "index": "pypi", + "version": "==7.4.4" + }, + "pytest-cov": { + "hashes": [ + "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6", + "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a" + ], + "index": "pypi", + "version": "==4.1.0" + }, + "pytype": { + "hashes": [ + "sha256:0e59fb2866cdd804b31ef9baa0f53bc6fddaeaf68a7b1af6aa62c1b9854f017e", + "sha256:144e83bb7b80e5b0972a25bed2e4f03c0cd91d677ad0902bb775fdac156151ca", + "sha256:20680085e5a7beee2aaedddbf96863efa96ff047061ae82e31404336180502f5", + "sha256:33227cd847df1c5e92dbe013dc3926b78c77b36032a1caa3e488286aa9a0a14e", + "sha256:5281cc89ba5acc5a9184845f5c02319c0fbfcd87a9ab4920b6bce0d0caec6860", + "sha256:734d34b3ce13ccea64c419f88630fd189d6d06cbb24516cfb6fe36edc1860da2", + "sha256:a964a105af46fff3495be76ee34ad34acf47300018244122ea75d0e777d04cb5", + "sha256:af4b7ced6049e7fececb646262a25874e8a4e6a0b7e540c100b673aacd230ffa", + "sha256:b1b802867ede7cfd7dbe479cfdd3a1341dac005bfcb2718ff22070494fdd57be", + "sha256:b1c5857acd6348e9f5ace6427d74824f433d6bbfad0e0104b98f5078f5781af6", + "sha256:bf28233b140e9a7702cffcc8346c8d47b492813bee5f11ba2cef906ff4c05c55", + "sha256:c22db76f45a218c673f70c4ab32e7de757b808ce5d9ae55d1b3621f05187c496" + ], + "index": "pypi", + "version": "==2024.1.5" + }, + "pyyaml": { + "hashes": [ + "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", + "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", + "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", + "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", + "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", + "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", + "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", + "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", + "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", + "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", + "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", + "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", + "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", + "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", + "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", + "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", + "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", + "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", + "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", + "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", + "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", + "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", + "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", + "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", + "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", + "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", + "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", + "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", + "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", + "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", + "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", + "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", + "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", + "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", + "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", + "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", + "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", + "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", + "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", + "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", + "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", + "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", + "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", + "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", + "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", + "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", + "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", + "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", + "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", + "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" + ], + "index": "pypi", + "version": "==6.0.1" + }, + "tabulate": { + "hashes": [ + "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", + "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f" + ], + "markers": "python_version >= '3.7'", + "version": "==0.9.0" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.10.2" + }, + "tomlkit": { + "hashes": [ + "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4", + "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba" + ], + "markers": "python_version >= '3.7'", + "version": "==0.12.3" + }, + "typing-extensions": { + "hashes": [ + "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", + "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + ], + "markers": "python_version >= '3.8'", + "version": "==4.9.0" + }, + "typing-inspect": { + "hashes": [ + "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", + "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78" + ], + "version": "==0.9.0" + } + } +} diff --git a/lint-workflow-v2/README.md b/lint-workflow-v2/README.md new file mode 100644 index 00000000..4134ac42 --- /dev/null +++ b/lint-workflow-v2/README.md @@ -0,0 +1,123 @@ +# lint-workflow + +## Usage + +There is currently NO packaging or distribution of this CLI tool. Until such time, the `cli.py` file needs to be run +with python 3.11+. + +`python cli.py --help` + + +``` +usage: workflow-linter [-h] [-v] {lint,actions} ... + +positional arguments: + {lint,actions} + lint Verify that a GitHub Action Workflow follows all of the Rules. + actions Add or Update Actions in the pre-approved list. + +options: + -h, --help show this help message and exit + -v, --verbose +``` + +## Development +### Requirements + +- Python 3.11 +- pipenv + +### Setup + +``` +pipenv install --dev +pipenv shell +``` + +### Testing + +All built-in `src/rules` should have 100% code coverage and we should shoot for an overall coverage of 80%+. +We are lax on the +[imperative shell](https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell) +(code interacting with other systems; ie. disk, network, etc), but we strive to maintain a high coverage over the +funcationl core (objects and models). + +``` +pipenv shell +pytest tests --cov=src +``` + +### Code Reformatting + +We adhere to PEP8 and use `black` to maintain this adherence. `black` should be run on any change being merged +to `main`. + +``` +pipenv shell +black . +``` + +### Linting + +We loosely use [Google's Python style guide](https://google.github.io/styleguide/pyguide.html), but yield to +`black` when there is a conflict + +``` +pipenv shell +pylint --rcfile pylintrc src/ tests/ +``` + +### Add a new Rule + +A new Rule is created by extending the Rule base class and overriding the `fn(obj: Union[Workflow, Job, Step])` method. +Available attributes of `Workflows`, `Jobs` and `Steps` can be found in their definitons under `src/models`. + +For a simple example, we'll take a look at enforcing the existence of the `name` key in a Job. This is already done by +default with the src.rules.name_exists.RuleNameExists, but provides a simple enough example to walk through. + +```python +from typing import Union, Tuple + +from ..rule import Rule +from ..models.job import Job +from ..models.workflow import Workflow +from ..models.step import Step +from ..utils import LintLevels, Settings + + +class RuleJobNameExists(Rule): + def __init__(self, settings: Settings = None) -> None: + self.message = "name must exist" + self.on_fail: LintLevels = LintLevels.ERROR + self.compatibility: List[Union[Workflow, Job, Step]] = [Job] + self.settings: Settings = settings + + def fn(self, obj: Job) -> Tuple[bool, str]: + """ """ + if obj.name is not None: + return True, "" + return False, self.message +``` + +[TODO: Is this enough documentation on how to use?] + +By default, a new Rule needs five things: + +- `self.message`: The message to return to the user on a lint failure +- `self.on_fail`: The level of failure on a lint failure (NONE, WARNING, ERROR). + NONE and WARNING will exit with a code of 0 (unless using `strict` mode for WARNING). + ERROR will exit with a non-zero exit code +- `self.compatibility`: The list of objects this rule is compatible with. This is used to create separate instances of + the Rule for each object in the Rules collection. +- `self.settings`: In general, this should default to what is shown here, but allows for overrides +- `self.fn`: The function doing the actual work to check the object and enforce the standardenforcing. + +`fn` can be as simple or as complex as it needs to be to run a check on a _single_ object. This linter currently does +not support Rules that check against multiple objects at a time OR file level formatting (one empty between each step or +two empty lines between each job) + + +### ToDo + +- [ ] Add Rule to assert correct format for single line run + diff --git a/lint-workflow/Taskfile.yml b/lint-workflow-v2/Taskfile.yml similarity index 100% rename from lint-workflow/Taskfile.yml rename to lint-workflow-v2/Taskfile.yml diff --git a/lint-workflow-v2/action.yml b/lint-workflow-v2/action.yml new file mode 100644 index 00000000..0ecde6ff --- /dev/null +++ b/lint-workflow-v2/action.yml @@ -0,0 +1,32 @@ +name: 'Lint Workflow' +description: 'Lints GitHub Actions Workflow' +inputs: + workflows: + description: "Path to workflow file(s)" + required: true +runs: + using: "composite" + steps: + - name: Install dependencies + run: pip install --user yamllint + shell: bash + + - name: Setup + id: setup + run: | + FORMAT_PATH=$(echo ${{ inputs.workflows }} | sed 's/ *$//') + echo "path=$FORMAT_PATH" >> $GITHUB_OUTPUT + shell: bash + + - name: Python lint + run: python ${{ github.action_path }}/lint.py "${{ steps.setup.outputs.path }}" + shell: bash + + - name: YAML lint + run: | + WORKFLOWS=($(echo "${{ steps.setup.outputs.path }}" | tr ' ' '\n')) + for WORKFLOW in "${WORKFLOWS[@]}"; do + yamllint -f colored -c ${{ github.action_path }}/.yamllint.yml $WORKFLOW + done + shell: bash + working-directory: ${{ github.workspace }} diff --git a/lint-workflow/actions.json b/lint-workflow-v2/actions.json similarity index 100% rename from lint-workflow/actions.json rename to lint-workflow-v2/actions.json diff --git a/lint-workflow/cli.py b/lint-workflow-v2/cli.py similarity index 100% rename from lint-workflow/cli.py rename to lint-workflow-v2/cli.py diff --git a/lint-workflow/flake.lock b/lint-workflow-v2/flake.lock similarity index 100% rename from lint-workflow/flake.lock rename to lint-workflow-v2/flake.lock diff --git a/lint-workflow/flake.nix b/lint-workflow-v2/flake.nix similarity index 100% rename from lint-workflow/flake.nix rename to lint-workflow-v2/flake.nix diff --git a/lint-workflow/pylintrc b/lint-workflow-v2/pylintrc similarity index 100% rename from lint-workflow/pylintrc rename to lint-workflow-v2/pylintrc diff --git a/lint-workflow/settings.py b/lint-workflow-v2/settings.py similarity index 100% rename from lint-workflow/settings.py rename to lint-workflow-v2/settings.py diff --git a/lint-workflow/src/__init__.py b/lint-workflow-v2/src/__init__.py similarity index 100% rename from lint-workflow/src/__init__.py rename to lint-workflow-v2/src/__init__.py diff --git a/lint-workflow/src/actions.py b/lint-workflow-v2/src/actions.py similarity index 100% rename from lint-workflow/src/actions.py rename to lint-workflow-v2/src/actions.py diff --git a/lint-workflow/src/lint.py b/lint-workflow-v2/src/lint.py similarity index 100% rename from lint-workflow/src/lint.py rename to lint-workflow-v2/src/lint.py diff --git a/lint-workflow/src/load.py b/lint-workflow-v2/src/load.py similarity index 100% rename from lint-workflow/src/load.py rename to lint-workflow-v2/src/load.py diff --git a/lint-workflow/src/models/__init__.py b/lint-workflow-v2/src/models/__init__.py similarity index 100% rename from lint-workflow/src/models/__init__.py rename to lint-workflow-v2/src/models/__init__.py diff --git a/lint-workflow/src/models/job.py b/lint-workflow-v2/src/models/job.py similarity index 100% rename from lint-workflow/src/models/job.py rename to lint-workflow-v2/src/models/job.py diff --git a/lint-workflow/src/models/step.py b/lint-workflow-v2/src/models/step.py similarity index 100% rename from lint-workflow/src/models/step.py rename to lint-workflow-v2/src/models/step.py diff --git a/lint-workflow/src/models/workflow.py b/lint-workflow-v2/src/models/workflow.py similarity index 100% rename from lint-workflow/src/models/workflow.py rename to lint-workflow-v2/src/models/workflow.py diff --git a/lint-workflow/src/rule.py b/lint-workflow-v2/src/rule.py similarity index 100% rename from lint-workflow/src/rule.py rename to lint-workflow-v2/src/rule.py diff --git a/lint-workflow/src/rules/__init__.py b/lint-workflow-v2/src/rules/__init__.py similarity index 100% rename from lint-workflow/src/rules/__init__.py rename to lint-workflow-v2/src/rules/__init__.py diff --git a/lint-workflow/src/rules/job_environment_prefix.py b/lint-workflow-v2/src/rules/job_environment_prefix.py similarity index 100% rename from lint-workflow/src/rules/job_environment_prefix.py rename to lint-workflow-v2/src/rules/job_environment_prefix.py diff --git a/lint-workflow/src/rules/name_capitalized.py b/lint-workflow-v2/src/rules/name_capitalized.py similarity index 100% rename from lint-workflow/src/rules/name_capitalized.py rename to lint-workflow-v2/src/rules/name_capitalized.py diff --git a/lint-workflow/src/rules/name_exists.py b/lint-workflow-v2/src/rules/name_exists.py similarity index 100% rename from lint-workflow/src/rules/name_exists.py rename to lint-workflow-v2/src/rules/name_exists.py diff --git a/lint-workflow/src/rules/pinned_job_runner.py b/lint-workflow-v2/src/rules/pinned_job_runner.py similarity index 100% rename from lint-workflow/src/rules/pinned_job_runner.py rename to lint-workflow-v2/src/rules/pinned_job_runner.py diff --git a/lint-workflow/src/rules/step_approved.py b/lint-workflow-v2/src/rules/step_approved.py similarity index 100% rename from lint-workflow/src/rules/step_approved.py rename to lint-workflow-v2/src/rules/step_approved.py diff --git a/lint-workflow/src/rules/step_pinned.py b/lint-workflow-v2/src/rules/step_pinned.py similarity index 100% rename from lint-workflow/src/rules/step_pinned.py rename to lint-workflow-v2/src/rules/step_pinned.py diff --git a/lint-workflow/src/utils.py b/lint-workflow-v2/src/utils.py similarity index 100% rename from lint-workflow/src/utils.py rename to lint-workflow-v2/src/utils.py diff --git a/lint-workflow/tests/rules/__init__.py b/lint-workflow-v2/tests/__init__.py similarity index 100% rename from lint-workflow/tests/rules/__init__.py rename to lint-workflow-v2/tests/__init__.py diff --git a/lint-workflow/tests/conftest.py b/lint-workflow-v2/tests/conftest.py similarity index 100% rename from lint-workflow/tests/conftest.py rename to lint-workflow-v2/tests/conftest.py diff --git a/lint-workflow-v2/tests/fixtures/test-alt.yml b/lint-workflow-v2/tests/fixtures/test-alt.yml new file mode 100644 index 00000000..7d01fafe --- /dev/null +++ b/lint-workflow-v2/tests/fixtures/test-alt.yml @@ -0,0 +1,25 @@ +--- +name: Lint Test File, DO NOT USE + +on: + workflow_dispatch: + inputs: {} + +jobs: + + test-normal-action: + name: Download Latest + runs-on: ubuntu-20.04 + steps: + - name: Checkout + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + + - run: | + echo test + + test-local-action: + name: Testing a local action call + runs-on: ubuntu-20.04 + steps: + - name: local-action + uses: ./version-bump diff --git a/lint-workflow/tests/fixtures/test-min-incorrect.yaml b/lint-workflow-v2/tests/fixtures/test-min-incorrect.yaml similarity index 100% rename from lint-workflow/tests/fixtures/test-min-incorrect.yaml rename to lint-workflow-v2/tests/fixtures/test-min-incorrect.yaml diff --git a/lint-workflow/tests/fixtures/test-min.yaml b/lint-workflow-v2/tests/fixtures/test-min.yaml similarity index 100% rename from lint-workflow/tests/fixtures/test-min.yaml rename to lint-workflow-v2/tests/fixtures/test-min.yaml diff --git a/lint-workflow-v2/tests/fixtures/test.yml b/lint-workflow-v2/tests/fixtures/test.yml new file mode 100644 index 00000000..cd6b71a2 --- /dev/null +++ b/lint-workflow-v2/tests/fixtures/test.yml @@ -0,0 +1,55 @@ +--- +name: crowdin Pull + +on: + workflow_dispatch: + inputs: {} + schedule: + - cron: "0 0 * * 5" + +jobs: + crowdin-pull: + name: Pull + runs-on: ubuntu-20.04 + env: + _CROWDIN_PROJECT_ID: "308189" + steps: + - name: Checkout repo + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.3.4 + + - name: Login to Azure + uses: Azure/logi@77f1b2e3fb80c0e8645114159d17008b8a2e475a + with: + creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + + - name: Retrieve secrets + id: retrieve-secrets + env: + KEYVAULT: bitwarden-prod-kv + SECRETS: | + crowdin-api-token + run: | + for i in ${SECRETS//,/ } + do + VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $i --query value --output tsv) + echo "::add-mask::$VALUE" + echo "$i=$VALUE" >> $GITHUB_OUTPUT + done + + - uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea # v1.3.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} + with: + config: crowdin.yml + crowdin_branch_name: master + upload_sources: false + upload_translations: false + download_translations: true + github_user_name: "github-actions" + github_user_email: "<>" + commit_message: "Autosync the updated translations" + localization_branch_name: crowdin-auto-sync + create_pull_request: true + pull_request_title: "Autosync Crowdin Translations" + pull_request_body: "Autosync the updated translations" diff --git a/lint-workflow-v2/tests/fixtures/test_a.yaml b/lint-workflow-v2/tests/fixtures/test_a.yaml new file mode 100644 index 00000000..bd0cfb24 --- /dev/null +++ b/lint-workflow-v2/tests/fixtures/test_a.yaml @@ -0,0 +1,27 @@ +--- +name: Lint Test File, DO NOT USE + +on: + workflow_dispatch: + inputs: {} + +jobs: + call-workflow: + uses: bitwarden/server/.github/workflows/workflow-linter.yml@master + + test-normal-action: + name: Download Latest + runs-on: ubuntu-20.04 + steps: + - name: Checkout + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + + - run: | + echo test + + test-local-action: + name: Testing a local action call + runs-on: ubuntu-20.04 + steps: + - name: local-action + uses: ./version-bump diff --git a/lint-workflow-v2/tests/rules/__init__.py b/lint-workflow-v2/tests/rules/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lint-workflow/tests/rules/test_job_environment_prefix.py b/lint-workflow-v2/tests/rules/test_job_environment_prefix.py similarity index 100% rename from lint-workflow/tests/rules/test_job_environment_prefix.py rename to lint-workflow-v2/tests/rules/test_job_environment_prefix.py diff --git a/lint-workflow/tests/rules/test_name_capitalized.py b/lint-workflow-v2/tests/rules/test_name_capitalized.py similarity index 100% rename from lint-workflow/tests/rules/test_name_capitalized.py rename to lint-workflow-v2/tests/rules/test_name_capitalized.py diff --git a/lint-workflow/tests/rules/test_name_exists.py b/lint-workflow-v2/tests/rules/test_name_exists.py similarity index 100% rename from lint-workflow/tests/rules/test_name_exists.py rename to lint-workflow-v2/tests/rules/test_name_exists.py diff --git a/lint-workflow/tests/rules/test_pinned_job_runner.py b/lint-workflow-v2/tests/rules/test_pinned_job_runner.py similarity index 100% rename from lint-workflow/tests/rules/test_pinned_job_runner.py rename to lint-workflow-v2/tests/rules/test_pinned_job_runner.py diff --git a/lint-workflow/tests/rules/test_step_approved.py b/lint-workflow-v2/tests/rules/test_step_approved.py similarity index 100% rename from lint-workflow/tests/rules/test_step_approved.py rename to lint-workflow-v2/tests/rules/test_step_approved.py diff --git a/lint-workflow/tests/rules/test_step_pinned.py b/lint-workflow-v2/tests/rules/test_step_pinned.py similarity index 100% rename from lint-workflow/tests/rules/test_step_pinned.py rename to lint-workflow-v2/tests/rules/test_step_pinned.py diff --git a/lint-workflow/tests/test_job.py b/lint-workflow-v2/tests/test_job.py similarity index 100% rename from lint-workflow/tests/test_job.py rename to lint-workflow-v2/tests/test_job.py diff --git a/lint-workflow-v2/tests/test_lint.py b/lint-workflow-v2/tests/test_lint.py new file mode 100644 index 00000000..d9a28e37 --- /dev/null +++ b/lint-workflow-v2/tests/test_lint.py @@ -0,0 +1,43 @@ +"""Test src/lint.py.""" +import pytest + +from src.lint import LinterCmd +from src.utils import Settings, LintFinding, LintLevels + + +@pytest.fixture(name="settings") +def fixture_settings(): + return Settings() + + +def test_get_max_error_level(settings): + linter = LinterCmd(settings=settings) + + assert ( + linter.get_max_error_level( + [ + LintFinding(level=LintLevels.WARNING), + LintFinding(level=LintLevels.WARNING), + ] + ) + == 1 + ) + + assert ( + linter.get_max_error_level( + [LintFinding(level=LintLevels.ERROR), LintFinding(level=LintLevels.ERROR)] + ) + == 2 + ) + + assert ( + linter.get_max_error_level( + [ + LintFinding(level=LintLevels.ERROR), + LintFinding(level=LintLevels.ERROR), + LintFinding(level=LintLevels.WARNING), + LintFinding(level=LintLevels.WARNING), + ] + ) + == 2 + ) diff --git a/lint-workflow/tests/test_load.py b/lint-workflow-v2/tests/test_load.py similarity index 100% rename from lint-workflow/tests/test_load.py rename to lint-workflow-v2/tests/test_load.py diff --git a/lint-workflow/tests/test_rule.py b/lint-workflow-v2/tests/test_rule.py similarity index 100% rename from lint-workflow/tests/test_rule.py rename to lint-workflow-v2/tests/test_rule.py diff --git a/lint-workflow/tests/test_step.py b/lint-workflow-v2/tests/test_step.py similarity index 100% rename from lint-workflow/tests/test_step.py rename to lint-workflow-v2/tests/test_step.py diff --git a/lint-workflow/tests/test_utils.py b/lint-workflow-v2/tests/test_utils.py similarity index 100% rename from lint-workflow/tests/test_utils.py rename to lint-workflow-v2/tests/test_utils.py diff --git a/lint-workflow/tests/test_workflow.py b/lint-workflow-v2/tests/test_workflow.py similarity index 100% rename from lint-workflow/tests/test_workflow.py rename to lint-workflow-v2/tests/test_workflow.py diff --git a/lint-workflow/Pipfile b/lint-workflow/Pipfile index 8f75f162..de96a7f7 100644 --- a/lint-workflow/Pipfile +++ b/lint-workflow/Pipfile @@ -6,17 +6,10 @@ name = "pypi" [packages] pyyaml = "*" urllib3 = "*" -pydantic = "*" -"ruamel.yaml" = "*" -dataclasses-json = "*" [dev-packages] black = "*" pytest = "*" -coverage = "*" -pytest-cov = "*" -pylint = "*" -pytype = "*" [requires] -python_version = "3.11" +python_version = "3.9" diff --git a/lint-workflow/Pipfile.lock b/lint-workflow/Pipfile.lock index d891658d..81a939c5 100644 --- a/lint-workflow/Pipfile.lock +++ b/lint-workflow/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "8b34ae1118eb5c9fac2c552c644f66f9dbd05415e08adf78efb99ba8a7f072f3" + "sha256": "bc7e46794f16595c77443451de92e409bd86605cdc1d788ee138dab0c5bc13d8" }, "pipfile-spec": 6, "requires": { - "python_version": "3.11" + "python_version": "3.9" }, "sources": [ { @@ -16,165 +16,6 @@ ] }, "default": { - "annotated-types": { - "hashes": [ - "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43", - "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d" - ], - "markers": "python_version >= '3.8'", - "version": "==0.6.0" - }, - "dataclasses-json": { - "hashes": [ - "sha256:35cb40aae824736fdf959801356641836365219cfe14caeb115c39136f775d2a", - "sha256:4aeb343357997396f6bca1acae64e486c3a723d8f5c76301888abeccf0c45176" - ], - "index": "pypi", - "version": "==0.6.3" - }, - "marshmallow": { - "hashes": [ - "sha256:4c1daff273513dc5eb24b219a8035559dc573c8f322558ef85f5438ddd1236dd", - "sha256:c21d4b98fee747c130e6bc8f45c4b3199ea66bc00c12ee1f639f0aeca034d5e9" - ], - "markers": "python_version >= '3.8'", - "version": "==3.20.2" - }, - "mypy-extensions": { - "hashes": [ - "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", - "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" - ], - "markers": "python_version >= '3.5'", - "version": "==1.0.0" - }, - "packaging": { - "hashes": [ - "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", - "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" - ], - "markers": "python_version >= '3.7'", - "version": "==23.2" - }, - "pydantic": { - "hashes": [ - "sha256:b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a", - "sha256:d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4" - ], - "index": "pypi", - "version": "==2.5.3" - }, - "pydantic-core": { - "hashes": [ - "sha256:00646784f6cd993b1e1c0e7b0fdcbccc375d539db95555477771c27555e3c556", - "sha256:00b1087dabcee0b0ffd104f9f53d7d3eaddfaa314cdd6726143af6bc713aa27e", - "sha256:0348b1dc6b76041516e8a854ff95b21c55f5a411c3297d2ca52f5528e49d8411", - "sha256:036137b5ad0cb0004c75b579445a1efccd072387a36c7f217bb8efd1afbe5245", - "sha256:095b707bb287bfd534044166ab767bec70a9bba3175dcdc3371782175c14e43c", - "sha256:0c08de15d50fa190d577e8591f0329a643eeaed696d7771760295998aca6bc66", - "sha256:1302a54f87b5cd8528e4d6d1bf2133b6aa7c6122ff8e9dc5220fbc1e07bffebd", - "sha256:172de779e2a153d36ee690dbc49c6db568d7b33b18dc56b69a7514aecbcf380d", - "sha256:1b027c86c66b8627eb90e57aee1f526df77dc6d8b354ec498be9a757d513b92b", - "sha256:1ce830e480f6774608dedfd4a90c42aac4a7af0a711f1b52f807130c2e434c06", - "sha256:1fd0c1d395372843fba13a51c28e3bb9d59bd7aebfeb17358ffaaa1e4dbbe948", - "sha256:23598acb8ccaa3d1d875ef3b35cb6376535095e9405d91a3d57a8c7db5d29341", - "sha256:24368e31be2c88bd69340fbfe741b405302993242ccb476c5c3ff48aeee1afe0", - "sha256:26a92ae76f75d1915806b77cf459811e772d8f71fd1e4339c99750f0e7f6324f", - "sha256:27e524624eace5c59af499cd97dc18bb201dc6a7a2da24bfc66ef151c69a5f2a", - "sha256:2b8719037e570639e6b665a4050add43134d80b687288ba3ade18b22bbb29dd2", - "sha256:2c5bcf3414367e29f83fd66f7de64509a8fd2368b1edf4351e862910727d3e51", - "sha256:2dbe357bc4ddda078f79d2a36fc1dd0494a7f2fad83a0a684465b6f24b46fe80", - "sha256:2f5fa187bde8524b1e37ba894db13aadd64faa884657473b03a019f625cee9a8", - "sha256:2f6ffc6701a0eb28648c845f4945a194dc7ab3c651f535b81793251e1185ac3d", - "sha256:314ccc4264ce7d854941231cf71b592e30d8d368a71e50197c905874feacc8a8", - "sha256:36026d8f99c58d7044413e1b819a67ca0e0b8ebe0f25e775e6c3d1fabb3c38fb", - "sha256:36099c69f6b14fc2c49d7996cbf4f87ec4f0e66d1c74aa05228583225a07b590", - "sha256:36fa402dcdc8ea7f1b0ddcf0df4254cc6b2e08f8cd80e7010d4c4ae6e86b2a87", - "sha256:370ffecb5316ed23b667d99ce4debe53ea664b99cc37bfa2af47bc769056d534", - "sha256:3860c62057acd95cc84044e758e47b18dcd8871a328ebc8ccdefd18b0d26a21b", - "sha256:399ac0891c284fa8eb998bcfa323f2234858f5d2efca3950ae58c8f88830f145", - "sha256:3a0b5db001b98e1c649dd55afa928e75aa4087e587b9524a4992316fa23c9fba", - "sha256:3dcf1978be02153c6a31692d4fbcc2a3f1db9da36039ead23173bc256ee3b91b", - "sha256:4241204e4b36ab5ae466ecec5c4c16527a054c69f99bba20f6f75232a6a534e2", - "sha256:438027a975cc213a47c5d70672e0d29776082155cfae540c4e225716586be75e", - "sha256:43e166ad47ba900f2542a80d83f9fc65fe99eb63ceec4debec160ae729824052", - "sha256:478e9e7b360dfec451daafe286998d4a1eeaecf6d69c427b834ae771cad4b622", - "sha256:4ce8299b481bcb68e5c82002b96e411796b844d72b3e92a3fbedfe8e19813eab", - "sha256:4f86f1f318e56f5cbb282fe61eb84767aee743ebe32c7c0834690ebea50c0a6b", - "sha256:55a23dcd98c858c0db44fc5c04fc7ed81c4b4d33c653a7c45ddaebf6563a2f66", - "sha256:599c87d79cab2a6a2a9df4aefe0455e61e7d2aeede2f8577c1b7c0aec643ee8e", - "sha256:5aa90562bc079c6c290f0512b21768967f9968e4cfea84ea4ff5af5d917016e4", - "sha256:64634ccf9d671c6be242a664a33c4acf12882670b09b3f163cd00a24cffbd74e", - "sha256:667aa2eac9cd0700af1ddb38b7b1ef246d8cf94c85637cbb03d7757ca4c3fdec", - "sha256:6a31d98c0d69776c2576dda4b77b8e0c69ad08e8b539c25c7d0ca0dc19a50d6c", - "sha256:6af4b3f52cc65f8a0bc8b1cd9676f8c21ef3e9132f21fed250f6958bd7223bed", - "sha256:6c8edaea3089bf908dd27da8f5d9e395c5b4dc092dbcce9b65e7156099b4b937", - "sha256:71d72ca5eaaa8d38c8df16b7deb1a2da4f650c41b58bb142f3fb75d5ad4a611f", - "sha256:72f9a942d739f09cd42fffe5dc759928217649f070056f03c70df14f5770acf9", - "sha256:747265448cb57a9f37572a488a57d873fd96bf51e5bb7edb52cfb37124516da4", - "sha256:75ec284328b60a4e91010c1acade0c30584f28a1f345bc8f72fe8b9e46ec6a96", - "sha256:78d0768ee59baa3de0f4adac9e3748b4b1fffc52143caebddfd5ea2961595277", - "sha256:78ee52ecc088c61cce32b2d30a826f929e1708f7b9247dc3b921aec367dc1b23", - "sha256:7be719e4d2ae6c314f72844ba9d69e38dff342bc360379f7c8537c48e23034b7", - "sha256:7e1f4744eea1501404b20b0ac059ff7e3f96a97d3e3f48ce27a139e053bb370b", - "sha256:7e90d6cc4aad2cc1f5e16ed56e46cebf4877c62403a311af20459c15da76fd91", - "sha256:7ebe3416785f65c28f4f9441e916bfc8a54179c8dea73c23023f7086fa601c5d", - "sha256:7f41533d7e3cf9520065f610b41ac1c76bc2161415955fbcead4981b22c7611e", - "sha256:7f5025db12fc6de7bc1104d826d5aee1d172f9ba6ca936bf6474c2148ac336c1", - "sha256:86c963186ca5e50d5c8287b1d1c9d3f8f024cbe343d048c5bd282aec2d8641f2", - "sha256:86ce5fcfc3accf3a07a729779d0b86c5d0309a4764c897d86c11089be61da160", - "sha256:8a14c192c1d724c3acbfb3f10a958c55a2638391319ce8078cb36c02283959b9", - "sha256:8b93785eadaef932e4fe9c6e12ba67beb1b3f1e5495631419c784ab87e975670", - "sha256:8ed1af8692bd8d2a29d702f1a2e6065416d76897d726e45a1775b1444f5928a7", - "sha256:92879bce89f91f4b2416eba4429c7b5ca22c45ef4a499c39f0c5c69257522c7c", - "sha256:94fc0e6621e07d1e91c44e016cc0b189b48db053061cc22d6298a611de8071bb", - "sha256:982487f8931067a32e72d40ab6b47b1628a9c5d344be7f1a4e668fb462d2da42", - "sha256:9862bf828112e19685b76ca499b379338fd4c5c269d897e218b2ae8fcb80139d", - "sha256:99b14dbea2fdb563d8b5a57c9badfcd72083f6006caf8e126b491519c7d64ca8", - "sha256:9c6a5c79b28003543db3ba67d1df336f253a87d3112dac3a51b94f7d48e4c0e1", - "sha256:a19b794f8fe6569472ff77602437ec4430f9b2b9ec7a1105cfd2232f9ba355e6", - "sha256:a306cdd2ad3a7d795d8e617a58c3a2ed0f76c8496fb7621b6cd514eb1532cae8", - "sha256:a3dde6cac75e0b0902778978d3b1646ca9f438654395a362cb21d9ad34b24acf", - "sha256:a874f21f87c485310944b2b2734cd6d318765bcbb7515eead33af9641816506e", - "sha256:a983cca5ed1dd9a35e9e42ebf9f278d344603bfcb174ff99a5815f953925140a", - "sha256:aca48506a9c20f68ee61c87f2008f81f8ee99f8d7f0104bff3c47e2d148f89d9", - "sha256:b2602177668f89b38b9f84b7b3435d0a72511ddef45dc14446811759b82235a1", - "sha256:b3e5fe4538001bb82e2295b8d2a39356a84694c97cb73a566dc36328b9f83b40", - "sha256:b6ca36c12a5120bad343eef193cc0122928c5c7466121da7c20f41160ba00ba2", - "sha256:b89f4477d915ea43b4ceea6756f63f0288941b6443a2b28c69004fe07fde0d0d", - "sha256:b9a9d92f10772d2a181b5ca339dee066ab7d1c9a34ae2421b2a52556e719756f", - "sha256:c99462ffc538717b3e60151dfaf91125f637e801f5ab008f81c402f1dff0cd0f", - "sha256:cb92f9061657287eded380d7dc455bbf115430b3aa4741bdc662d02977e7d0af", - "sha256:cdee837710ef6b56ebd20245b83799fce40b265b3b406e51e8ccc5b85b9099b7", - "sha256:cf10b7d58ae4a1f07fccbf4a0a956d705356fea05fb4c70608bb6fa81d103cda", - "sha256:d15687d7d7f40333bd8266f3814c591c2e2cd263fa2116e314f60d82086e353a", - "sha256:d5c28525c19f5bb1e09511669bb57353d22b94cf8b65f3a8d141c389a55dec95", - "sha256:d5f916acf8afbcab6bacbb376ba7dc61f845367901ecd5e328fc4d4aef2fcab0", - "sha256:dab03ed811ed1c71d700ed08bde8431cf429bbe59e423394f0f4055f1ca0ea60", - "sha256:db453f2da3f59a348f514cfbfeb042393b68720787bbef2b4c6068ea362c8149", - "sha256:de2a0645a923ba57c5527497daf8ec5df69c6eadf869e9cd46e86349146e5975", - "sha256:dea7fcd62915fb150cdc373212141a30037e11b761fbced340e9db3379b892d4", - "sha256:dfcbebdb3c4b6f739a91769aea5ed615023f3c88cb70df812849aef634c25fbe", - "sha256:dfcebb950aa7e667ec226a442722134539e77c575f6cfaa423f24371bb8d2e94", - "sha256:e0641b506486f0b4cd1500a2a65740243e8670a2549bb02bc4556a83af84ae03", - "sha256:e33b0834f1cf779aa839975f9d8755a7c2420510c0fa1e9fa0497de77cd35d2c", - "sha256:e4ace1e220b078c8e48e82c081e35002038657e4b37d403ce940fa679e57113b", - "sha256:e4cf2d5829f6963a5483ec01578ee76d329eb5caf330ecd05b3edd697e7d768a", - "sha256:e574de99d735b3fc8364cba9912c2bec2da78775eba95cbb225ef7dda6acea24", - "sha256:e646c0e282e960345314f42f2cea5e0b5f56938c093541ea6dbf11aec2862391", - "sha256:e8a5ac97ea521d7bde7621d86c30e86b798cdecd985723c4ed737a2aa9e77d0c", - "sha256:eedf97be7bc3dbc8addcef4142f4b4164066df0c6f36397ae4aaed3eb187d8ab", - "sha256:ef633add81832f4b56d3b4c9408b43d530dfca29e68fb1b797dcb861a2c734cd", - "sha256:f27207e8ca3e5e021e2402ba942e5b4c629718e665c81b8b306f3c8b1ddbb786", - "sha256:f85f3843bdb1fe80e8c206fe6eed7a1caeae897e496542cee499c374a85c6e08", - "sha256:f8e81e4b55930e5ffab4a68db1af431629cf2e4066dbdbfef65348b8ab804ea8", - "sha256:f96ae96a060a8072ceff4cfde89d261837b4294a4f28b84a28765470d502ccc6", - "sha256:fd9e98b408384989ea4ab60206b8e100d8687da18b5c813c11e92fd8212a98e0", - "sha256:ffff855100bc066ff2cd3aa4a60bc9534661816b110f0243e59503ec2df38421" - ], - "markers": "python_version >= '3.7'", - "version": "==2.14.6" - }, "pyyaml": { "hashes": [ "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", @@ -229,140 +70,44 @@ "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==6.0.1" }, - "ruamel.yaml": { - "hashes": [ - "sha256:61917e3a35a569c1133a8f772e1226961bf5a1198bea7e23f06a0841dea1ab0e", - "sha256:a013ac02f99a69cdd6277d9664689eb1acba07069f912823177c5eced21a6ada" - ], - "index": "pypi", - "version": "==0.18.5" - }, - "ruamel.yaml.clib": { - "hashes": [ - "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d", - "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001", - "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462", - "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9", - "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe", - "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b", - "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b", - "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615", - "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62", - "sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15", - "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b", - "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1", - "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9", - "sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675", - "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899", - "sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7", - "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7", - "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312", - "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa", - "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91", - "sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b", - "sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6", - "sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3", - "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334", - "sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5", - "sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3", - "sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe", - "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c", - "sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed", - "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337", - "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880", - "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f", - "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d", - "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248", - "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d", - "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf", - "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512", - "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069", - "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb", - "sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942", - "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d", - "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31", - "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92", - "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5", - "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28", - "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d", - "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1", - "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2", - "sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875", - "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412" - ], - "markers": "python_version < '3.13' and platform_python_implementation == 'CPython'", - "version": "==0.2.8" - }, - "typing-extensions": { - "hashes": [ - "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", - "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" - ], - "markers": "python_version >= '3.8'", - "version": "==4.9.0" - }, - "typing-inspect": { - "hashes": [ - "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", - "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78" - ], - "version": "==0.9.0" - }, "urllib3": { "hashes": [ "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==2.1.0" } }, "develop": { - "astroid": { - "hashes": [ - "sha256:4a61cf0a59097c7bb52689b0fd63717cd2a8a14dc9f1eee97b82d814881c8c91", - "sha256:d6e62862355f60e716164082d6b4b041d38e2a8cf1c7cd953ded5108bac8ff5c" - ], - "markers": "python_full_version >= '3.8.0'", - "version": "==3.0.2" - }, - "attrs": { - "hashes": [ - "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", - "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" - ], - "markers": "python_version >= '3.7'", - "version": "==23.2.0" - }, "black": { "hashes": [ - "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50", - "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f", - "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e", - "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec", - "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055", - "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3", - "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5", - "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54", - "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b", - "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e", - "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e", - "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba", - "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea", - "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59", - "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d", - "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0", - "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9", - "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a", - "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e", - "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba", - "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2", - "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2" + "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4", + "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b", + "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f", + "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07", + "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187", + "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6", + "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05", + "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06", + "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e", + "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5", + "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244", + "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f", + "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221", + "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055", + "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479", + "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394", + "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911", + "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142" ], "index": "pypi", - "version": "==23.12.1" + "markers": "python_version >= '3.8'", + "version": "==23.11.0" }, "click": { "hashes": [ @@ -372,79 +117,13 @@ "markers": "python_version >= '3.7'", "version": "==8.1.7" }, - "coverage": { - "hashes": [ - "sha256:04387a4a6ecb330c1878907ce0dc04078ea72a869263e53c72a1ba5bbdf380ca", - "sha256:0676cd0ba581e514b7f726495ea75aba3eb20899d824636c6f59b0ed2f88c471", - "sha256:0e8d06778e8fbffccfe96331a3946237f87b1e1d359d7fbe8b06b96c95a5407a", - "sha256:0eb3c2f32dabe3a4aaf6441dde94f35687224dfd7eb2a7f47f3fd9428e421058", - "sha256:109f5985182b6b81fe33323ab4707011875198c41964f014579cf82cebf2bb85", - "sha256:13eaf476ec3e883fe3e5fe3707caeb88268a06284484a3daf8250259ef1ba143", - "sha256:164fdcc3246c69a6526a59b744b62e303039a81e42cfbbdc171c91a8cc2f9446", - "sha256:26776ff6c711d9d835557ee453082025d871e30b3fd6c27fcef14733f67f0590", - "sha256:26f66da8695719ccf90e794ed567a1549bb2644a706b41e9f6eae6816b398c4a", - "sha256:29f3abe810930311c0b5d1a7140f6395369c3db1be68345638c33eec07535105", - "sha256:316543f71025a6565677d84bc4df2114e9b6a615aa39fb165d697dba06a54af9", - "sha256:36b0ea8ab20d6a7564e89cb6135920bc9188fb5f1f7152e94e8300b7b189441a", - "sha256:3cc9d4bc55de8003663ec94c2f215d12d42ceea128da8f0f4036235a119c88ac", - "sha256:485e9f897cf4856a65a57c7f6ea3dc0d4e6c076c87311d4bc003f82cfe199d25", - "sha256:5040148f4ec43644702e7b16ca864c5314ccb8ee0751ef617d49aa0e2d6bf4f2", - "sha256:51456e6fa099a8d9d91497202d9563a320513fcf59f33991b0661a4a6f2ad450", - "sha256:53d7d9158ee03956e0eadac38dfa1ec8068431ef8058fe6447043db1fb40d932", - "sha256:5a10a4920def78bbfff4eff8a05c51be03e42f1c3735be42d851f199144897ba", - "sha256:5b14b4f8760006bfdb6e08667af7bc2d8d9bfdb648351915315ea17645347137", - "sha256:5b2ccb7548a0b65974860a78c9ffe1173cfb5877460e5a229238d985565574ae", - "sha256:697d1317e5290a313ef0d369650cfee1a114abb6021fa239ca12b4849ebbd614", - "sha256:6ae8c9d301207e6856865867d762a4b6fd379c714fcc0607a84b92ee63feff70", - "sha256:707c0f58cb1712b8809ece32b68996ee1e609f71bd14615bd8f87a1293cb610e", - "sha256:74775198b702868ec2d058cb92720a3c5a9177296f75bd97317c787daf711505", - "sha256:756ded44f47f330666843b5781be126ab57bb57c22adbb07d83f6b519783b870", - "sha256:76f03940f9973bfaee8cfba70ac991825611b9aac047e5c80d499a44079ec0bc", - "sha256:79287fd95585ed36e83182794a57a46aeae0b64ca53929d1176db56aacc83451", - "sha256:799c8f873794a08cdf216aa5d0531c6a3747793b70c53f70e98259720a6fe2d7", - "sha256:7d360587e64d006402b7116623cebf9d48893329ef035278969fa3bbf75b697e", - "sha256:80b5ee39b7f0131ebec7968baa9b2309eddb35b8403d1869e08f024efd883566", - "sha256:815ac2d0f3398a14286dc2cea223a6f338109f9ecf39a71160cd1628786bc6f5", - "sha256:83c2dda2666fe32332f8e87481eed056c8b4d163fe18ecc690b02802d36a4d26", - "sha256:846f52f46e212affb5bcf131c952fb4075b55aae6b61adc9856222df89cbe3e2", - "sha256:936d38794044b26c99d3dd004d8af0035ac535b92090f7f2bb5aa9c8e2f5cd42", - "sha256:9864463c1c2f9cb3b5db2cf1ff475eed2f0b4285c2aaf4d357b69959941aa555", - "sha256:995ea5c48c4ebfd898eacb098164b3cc826ba273b3049e4a889658548e321b43", - "sha256:a1526d265743fb49363974b7aa8d5899ff64ee07df47dd8d3e37dcc0818f09ed", - "sha256:a56de34db7b7ff77056a37aedded01b2b98b508227d2d0979d373a9b5d353daa", - "sha256:a7c97726520f784239f6c62506bc70e48d01ae71e9da128259d61ca5e9788516", - "sha256:b8e99f06160602bc64da35158bb76c73522a4010f0649be44a4e167ff8555952", - "sha256:bb1de682da0b824411e00a0d4da5a784ec6496b6850fdf8c865c1d68c0e318dd", - "sha256:bf477c355274a72435ceb140dc42de0dc1e1e0bf6e97195be30487d8eaaf1a09", - "sha256:bf635a52fc1ea401baf88843ae8708591aa4adff875e5c23220de43b1ccf575c", - "sha256:bfd5db349d15c08311702611f3dccbef4b4e2ec148fcc636cf8739519b4a5c0f", - "sha256:c530833afc4707fe48524a44844493f36d8727f04dcce91fb978c414a8556cc6", - "sha256:cc6d65b21c219ec2072c1293c505cf36e4e913a3f936d80028993dd73c7906b1", - "sha256:cd3c1e4cb2ff0083758f09be0f77402e1bdf704adb7f89108007300a6da587d0", - "sha256:cfd2a8b6b0d8e66e944d47cdec2f47c48fef2ba2f2dff5a9a75757f64172857e", - "sha256:d0ca5c71a5a1765a0f8f88022c52b6b8be740e512980362f7fdbb03725a0d6b9", - "sha256:e7defbb9737274023e2d7af02cac77043c86ce88a907c58f42b580a97d5bcca9", - "sha256:e9d1bf53c4c8de58d22e0e956a79a5b37f754ed1ffdbf1a260d9dcfa2d8a325e", - "sha256:ea81d8f9691bb53f4fb4db603203029643caffc82bf998ab5b59ca05560f4c06" - ], - "index": "pypi", - "version": "==7.4.0" - }, - "dill": { - "hashes": [ - "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e", - "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03" - ], - "markers": "python_version >= '3.11'", - "version": "==0.3.7" - }, - "importlab": { + "exceptiongroup": { "hashes": [ - "sha256:124cfa00e8a34fefe8aac1a5e94f56c781b178c9eb61a1d3f60f7e03b77338d3", - "sha256:b3893853b1f6eb027da509c3b40e6787e95dd66b4b66f1b3613aad77556e1465" + "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9", + "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3" ], - "markers": "python_full_version >= '3.6.0'", - "version": "==0.8.1" + "markers": "python_version < '3.11'", + "version": "==1.1.3" }, "iniconfig": { "hashes": [ @@ -454,133 +133,6 @@ "markers": "python_version >= '3.7'", "version": "==2.0.0" }, - "isort": { - "hashes": [ - "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", - "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6" - ], - "markers": "python_full_version >= '3.8.0'", - "version": "==5.13.2" - }, - "jinja2": { - "hashes": [ - "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa", - "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90" - ], - "markers": "python_version >= '3.7'", - "version": "==3.1.3" - }, - "libcst": { - "hashes": [ - "sha256:003e5e83a12eed23542c4ea20fdc8de830887cc03662432bb36f84f8c4841b81", - "sha256:0acbacb9a170455701845b7e940e2d7b9519db35a86768d86330a0b0deae1086", - "sha256:0bf69cbbab5016d938aac4d3ae70ba9ccb3f90363c588b3b97be434e6ba95403", - "sha256:2d37326bd6f379c64190a28947a586b949de3a76be00176b0732c8ee87d67ebe", - "sha256:3a07ecfabbbb8b93209f952a365549e65e658831e9231649f4f4e4263cad24b1", - "sha256:3ebbb9732ae3cc4ae7a0e97890bed0a57c11d6df28790c2b9c869f7da653c7c7", - "sha256:4bc745d0c06420fe2644c28d6ddccea9474fb68a2135904043676deb4fa1e6bc", - "sha256:5297a16e575be8173185e936b7765c89a3ca69d4ae217a4af161814a0f9745a7", - "sha256:5f1cd308a4c2f71d5e4eec6ee693819933a03b78edb2e4cc5e3ad1afd5fb3f07", - "sha256:63f75656fd733dc20354c46253fde3cf155613e37643c3eaf6f8818e95b7a3d1", - "sha256:73c086705ed34dbad16c62c9adca4249a556c1b022993d511da70ea85feaf669", - "sha256:75816647736f7e09c6120bdbf408456f99b248d6272277eed9a58cf50fb8bc7d", - "sha256:78b7a38ec4c1c009ac39027d51558b52851fb9234669ba5ba62283185963a31c", - "sha256:7ccaf53925f81118aeaadb068a911fac8abaff608817d7343da280616a5ca9c1", - "sha256:82d1271403509b0a4ee6ff7917c2d33b5a015f44d1e208abb1da06ba93b2a378", - "sha256:8ae11eb1ea55a16dc0cdc61b41b29ac347da70fec14cc4381248e141ee2fbe6c", - "sha256:8afb6101b8b3c86c5f9cec6b90ab4da16c3c236fe7396f88e8b93542bb341f7c", - "sha256:8c1f2da45f1c45634090fd8672c15e0159fdc46853336686959b2d093b6e10fa", - "sha256:97fbc73c87e9040e148881041fd5ffa2a6ebf11f64b4ccb5b52e574b95df1a15", - "sha256:99fdc1929703fd9e7408aed2e03f58701c5280b05c8911753a8d8619f7dfdda5", - "sha256:9dffa1795c2804d183efb01c0f1efd20a7831db6a21a0311edf90b4100d67436", - "sha256:bca1841693941fdd18371824bb19a9702d5784cd347cb8231317dbdc7062c5bc", - "sha256:c653d9121d6572d8b7f8abf20f88b0a41aab77ff5a6a36e5a0ec0f19af0072e8", - "sha256:c8f26250f87ca849a7303ed7a4fd6b2c7ac4dec16b7d7e68ca6a476d7c9bfcdb", - "sha256:cc9b6ac36d7ec9db2f053014ea488086ca2ed9c322be104fbe2c71ca759da4bb", - "sha256:d22d1abfe49aa60fc61fa867e10875a9b3024ba5a801112f4d7ba42d8d53242e", - "sha256:d68c34e3038d3d1d6324eb47744cbf13f2c65e1214cf49db6ff2a6603c1cd838", - "sha256:e3d8cf974cfa2487b28f23f56c4bff90d550ef16505e58b0dca0493d5293784b", - "sha256:f36f592e035ef84f312a12b75989dde6a5f6767fe99146cdae6a9ee9aff40dd0", - "sha256:f561c9a84eca18be92f4ad90aa9bd873111efbea995449301719a1a7805dbc5c", - "sha256:fe41b33aa73635b1651f64633f429f7aa21f86d2db5748659a99d9b7b1ed2a90" - ], - "markers": "python_version >= '3.8'", - "version": "==1.1.0" - }, - "markupsafe": { - "hashes": [ - "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e", - "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e", - "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431", - "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686", - "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c", - "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559", - "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc", - "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb", - "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939", - "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c", - "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0", - "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4", - "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9", - "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575", - "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba", - "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d", - "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd", - "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3", - "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00", - "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155", - "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac", - "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52", - "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f", - "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8", - "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b", - "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007", - "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24", - "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea", - "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198", - "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0", - "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee", - "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be", - "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2", - "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1", - "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707", - "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6", - "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c", - "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58", - "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823", - "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779", - "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636", - "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c", - "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad", - "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee", - "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc", - "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2", - "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48", - "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7", - "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e", - "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b", - "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa", - "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5", - "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e", - "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb", - "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9", - "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57", - "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc", - "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc", - "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2", - "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11" - ], - "markers": "python_version >= '3.7'", - "version": "==2.1.3" - }, - "mccabe": { - "hashes": [ - "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", - "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" - ], - "markers": "python_version >= '3.6'", - "version": "==0.7.0" - }, "mypy-extensions": { "hashes": [ "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", @@ -589,34 +141,6 @@ "markers": "python_version >= '3.5'", "version": "==1.0.0" }, - "networkx": { - "hashes": [ - "sha256:4f33f68cb2afcf86f28a45f43efc27a9386b535d567d2127f8f61d51dec58d36", - "sha256:de346335408f84de0eada6ff9fafafff9bcda11f0a0dfaa931133debb146ab61" - ], - "markers": "python_version >= '3.8'", - "version": "==3.1" - }, - "ninja": { - "hashes": [ - "sha256:18302d96a5467ea98b68e1cae1ae4b4fb2b2a56a82b955193c637557c7273dbd", - "sha256:185e0641bde601e53841525c4196278e9aaf4463758da6dd1e752c0a0f54136a", - "sha256:376889c76d87b95b5719fdd61dd7db193aa7fd4432e5d52d2e44e4c497bdbbee", - "sha256:3e0f9be5bb20d74d58c66cc1c414c3e6aeb45c35b0d0e41e8d739c2c0d57784f", - "sha256:73b93c14046447c7c5cc892433d4fae65d6364bec6685411cb97a8bcf815f93a", - "sha256:7563ce1d9fe6ed5af0b8dd9ab4a214bf4ff1f2f6fd6dc29f480981f0f8b8b249", - "sha256:76482ba746a2618eecf89d5253c0d1e4f1da1270d41e9f54dfbd91831b0f6885", - "sha256:84502ec98f02a037a169c4b0d5d86075eaf6afc55e1879003d6cab51ced2ea4b", - "sha256:95da904130bfa02ea74ff9c0116b4ad266174fafb1c707aa50212bc7859aebf1", - "sha256:9d793b08dd857e38d0b6ffe9e6b7145d7c485a42dcfea04905ca0cdb6017cc3c", - "sha256:9df724344202b83018abb45cb1efc22efd337a1496514e7e6b3b59655be85205", - "sha256:aad34a70ef15b12519946c5633344bc775a7656d789d9ed5fdb0d456383716ef", - "sha256:d491fc8d89cdcb416107c349ad1e3a735d4c4af5e1cb8f5f727baca6350fdaea", - "sha256:ecf80cf5afd09f14dcceff28cb3f11dc90fb97c999c89307aea435889cb66877", - "sha256:fa2ba9d74acfdfbfbcf06fad1b8282de8a7a8c481d9dee45c859a8c93fcc1082" - ], - "version": "==1.11.1.1" - }, "packaging": { "hashes": [ "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", @@ -627,19 +151,19 @@ }, "pathspec": { "hashes": [ - "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", - "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" + "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20", + "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3" ], - "markers": "python_version >= '3.8'", - "version": "==0.12.1" + "markers": "python_version >= '3.7'", + "version": "==0.11.2" }, "platformdirs": { "hashes": [ - "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380", - "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420" + "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b", + "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731" ], - "markers": "python_version >= '3.8'", - "version": "==4.1.0" + "markers": "python_version >= '3.7'", + "version": "==4.0.0" }, "pluggy": { "hashes": [ @@ -649,166 +173,30 @@ "markers": "python_version >= '3.8'", "version": "==1.3.0" }, - "pycnite": { - "hashes": [ - "sha256:7d02eb0ec4b405d8812ce053434dacfc2335dcd458ab58a1a8bf64f72d40bd76", - "sha256:ad8616982beecc39f2090999aa8fe0b044b1f6733ec39484cb5e0900b3c88aa1" - ], - "markers": "python_version >= '3.8'", - "version": "==2023.10.11" - }, - "pydot": { - "hashes": [ - "sha256:408a47913ea7bd5d2d34b274144880c1310c4aee901f353cf21fe2e526a4ea28", - "sha256:60246af215123fa062f21cd791be67dda23a6f280df09f68919e637a1e4f3235" - ], - "markers": "python_version >= '3.7'", - "version": "==2.0.0" - }, - "pylint": { - "hashes": [ - "sha256:58c2398b0301e049609a8429789ec6edf3aabe9b6c5fec916acd18639c16de8b", - "sha256:7a1585285aefc5165db81083c3e06363a27448f6b467b3b0f30dbd0ac1f73810" - ], - "index": "pypi", - "version": "==3.0.3" - }, - "pyparsing": { - "hashes": [ - "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb", - "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db" - ], - "markers": "python_full_version >= '3.6.8'", - "version": "==3.1.1" - }, "pytest": { "hashes": [ - "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", - "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8" - ], - "index": "pypi", - "version": "==7.4.4" - }, - "pytest-cov": { - "hashes": [ - "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6", - "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a" - ], - "index": "pypi", - "version": "==4.1.0" - }, - "pytype": { - "hashes": [ - "sha256:0e59fb2866cdd804b31ef9baa0f53bc6fddaeaf68a7b1af6aa62c1b9854f017e", - "sha256:144e83bb7b80e5b0972a25bed2e4f03c0cd91d677ad0902bb775fdac156151ca", - "sha256:20680085e5a7beee2aaedddbf96863efa96ff047061ae82e31404336180502f5", - "sha256:33227cd847df1c5e92dbe013dc3926b78c77b36032a1caa3e488286aa9a0a14e", - "sha256:5281cc89ba5acc5a9184845f5c02319c0fbfcd87a9ab4920b6bce0d0caec6860", - "sha256:734d34b3ce13ccea64c419f88630fd189d6d06cbb24516cfb6fe36edc1860da2", - "sha256:a964a105af46fff3495be76ee34ad34acf47300018244122ea75d0e777d04cb5", - "sha256:af4b7ced6049e7fececb646262a25874e8a4e6a0b7e540c100b673aacd230ffa", - "sha256:b1b802867ede7cfd7dbe479cfdd3a1341dac005bfcb2718ff22070494fdd57be", - "sha256:b1c5857acd6348e9f5ace6427d74824f433d6bbfad0e0104b98f5078f5781af6", - "sha256:bf28233b140e9a7702cffcc8346c8d47b492813bee5f11ba2cef906ff4c05c55", - "sha256:c22db76f45a218c673f70c4ab32e7de757b808ce5d9ae55d1b3621f05187c496" - ], - "index": "pypi", - "version": "==2024.1.5" - }, - "pyyaml": { - "hashes": [ - "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", - "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", - "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", - "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", - "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", - "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", - "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", - "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", - "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", - "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", - "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", - "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", - "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", - "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", - "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", - "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", - "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", - "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", - "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", - "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", - "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", - "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", - "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", - "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", - "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", - "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", - "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", - "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", - "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", - "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", - "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", - "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", - "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", - "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", - "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", - "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", - "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", - "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", - "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", - "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", - "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", - "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", - "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", - "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", - "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", - "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", - "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", - "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", - "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", - "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" + "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac", + "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5" ], "index": "pypi", - "version": "==6.0.1" - }, - "tabulate": { - "hashes": [ - "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", - "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f" - ], "markers": "python_version >= '3.7'", - "version": "==0.9.0" + "version": "==7.4.3" }, - "toml": { + "tomli": { "hashes": [ - "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", - "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.10.2" - }, - "tomlkit": { - "hashes": [ - "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4", - "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba" - ], - "markers": "python_version >= '3.7'", - "version": "==0.12.3" + "markers": "python_version < '3.11'", + "version": "==2.0.1" }, "typing-extensions": { "hashes": [ - "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", - "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" - ], - "markers": "python_version >= '3.8'", - "version": "==4.9.0" - }, - "typing-inspect": { - "hashes": [ - "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", - "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78" + "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0", + "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef" ], - "version": "==0.9.0" + "markers": "python_version < '3.11'", + "version": "==4.8.0" } } } diff --git a/lint-workflow/README.md b/lint-workflow/README.md index 4134ac42..48cd5eed 100644 --- a/lint-workflow/README.md +++ b/lint-workflow/README.md @@ -1,30 +1,9 @@ # lint-workflow -## Usage - -There is currently NO packaging or distribution of this CLI tool. Until such time, the `cli.py` file needs to be run -with python 3.11+. - -`python cli.py --help` - - -``` -usage: workflow-linter [-h] [-v] {lint,actions} ... - -positional arguments: - {lint,actions} - lint Verify that a GitHub Action Workflow follows all of the Rules. - actions Add or Update Actions in the pre-approved list. - -options: - -h, --help show this help message and exit - -v, --verbose -``` - ## Development ### Requirements -- Python 3.11 +- Python 3.9 - pipenv ### Setup @@ -36,88 +15,14 @@ pipenv shell ### Testing -All built-in `src/rules` should have 100% code coverage and we should shoot for an overall coverage of 80%+. -We are lax on the -[imperative shell](https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell) -(code interacting with other systems; ie. disk, network, etc), but we strive to maintain a high coverage over the -funcationl core (objects and models). - ``` pipenv shell -pytest tests --cov=src +pytest tests ``` ### Code Reformatting -We adhere to PEP8 and use `black` to maintain this adherence. `black` should be run on any change being merged -to `main`. - ``` pipenv shell black . ``` - -### Linting - -We loosely use [Google's Python style guide](https://google.github.io/styleguide/pyguide.html), but yield to -`black` when there is a conflict - -``` -pipenv shell -pylint --rcfile pylintrc src/ tests/ -``` - -### Add a new Rule - -A new Rule is created by extending the Rule base class and overriding the `fn(obj: Union[Workflow, Job, Step])` method. -Available attributes of `Workflows`, `Jobs` and `Steps` can be found in their definitons under `src/models`. - -For a simple example, we'll take a look at enforcing the existence of the `name` key in a Job. This is already done by -default with the src.rules.name_exists.RuleNameExists, but provides a simple enough example to walk through. - -```python -from typing import Union, Tuple - -from ..rule import Rule -from ..models.job import Job -from ..models.workflow import Workflow -from ..models.step import Step -from ..utils import LintLevels, Settings - - -class RuleJobNameExists(Rule): - def __init__(self, settings: Settings = None) -> None: - self.message = "name must exist" - self.on_fail: LintLevels = LintLevels.ERROR - self.compatibility: List[Union[Workflow, Job, Step]] = [Job] - self.settings: Settings = settings - - def fn(self, obj: Job) -> Tuple[bool, str]: - """ """ - if obj.name is not None: - return True, "" - return False, self.message -``` - -[TODO: Is this enough documentation on how to use?] - -By default, a new Rule needs five things: - -- `self.message`: The message to return to the user on a lint failure -- `self.on_fail`: The level of failure on a lint failure (NONE, WARNING, ERROR). - NONE and WARNING will exit with a code of 0 (unless using `strict` mode for WARNING). - ERROR will exit with a non-zero exit code -- `self.compatibility`: The list of objects this rule is compatible with. This is used to create separate instances of - the Rule for each object in the Rules collection. -- `self.settings`: In general, this should default to what is shown here, but allows for overrides -- `self.fn`: The function doing the actual work to check the object and enforce the standardenforcing. - -`fn` can be as simple or as complex as it needs to be to run a check on a _single_ object. This linter currently does -not support Rules that check against multiple objects at a time OR file level formatting (one empty between each step or -two empty lines between each job) - - -### ToDo - -- [ ] Add Rule to assert correct format for single line run - diff --git a/lint-workflow/lint.py b/lint-workflow/lint.py new file mode 100644 index 00000000..fffb433c --- /dev/null +++ b/lint-workflow/lint.py @@ -0,0 +1,439 @@ +import sys +import argparse +import os +import yaml +import json +import urllib3 as urllib +import logging + +PROBLEM_LEVELS = { + "warning": 1, + "error": 2, +} + + +memoized_action_update_urls = {} + + +class Colors: + """Class containing color codes for printing strings to output.""" + + black = "30m" + red = "31m" + green = "32m" + yellow = "33m" + blue = "34m" + magenta = "35m" + cyan = "36m" + white = "37m" + + +class LintFinding(object): + """Represents a linting problem.""" + + def __init__(self, description="", level=None): + self.description = description + self.level = level + + +def get_max_error_level(findings): + """Get max error level from list of findings.""" + if len(findings) == 0: + return 0 + max_problem = max(findings, key=lambda finding: PROBLEM_LEVELS[finding.level]) + max_problem_level = PROBLEM_LEVELS[max_problem.level] + return max_problem_level + + +def print_finding(finding: LintFinding): + """Print formatted and colored finding.""" + if finding.level == "warning": + color = Colors.yellow + elif finding.level == "error": + color = Colors.red + else: + color = Colors.white + + line = f" - \033[{color}{finding.level}\033[0m {finding.description}" + + print(line) + + +def get_github_api_response(url, action_id): + """Call GitHub API with error logging without throwing an exception.""" + http = urllib.PoolManager() + headers = {"user-agent": "bw-linter"} + + if os.getenv("GITHUB_TOKEN", None): + headers["Authorization"] = f"Token {os.environ['GITHUB_TOKEN']}" + + response = http.request("GET", url, headers=headers) + + if response.status == 403 and response.reason == "rate limit exceeded": + logging.error( + f"Failed to call GitHub API for action: {action_id} due to rate limit exceeded." + ) + return None + + if response.status == 401 and response.reason == "Unauthorized": + logging.error( + f"Failed to call GitHub API for action: {action_id}: {response.data}." + ) + return None + + return response + + +def action_repo_exists(action_id): + """ + Takes and action id and checks if the action repo exists. + + Example action_id: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945 + """ + + if "./" in action_id: + # Handle local workflow calls, return None since there will be no updates. + return True + + path, *hash = action_id.split("@") + + if "bitwarden/gh-actions" in path: + path_list = path.split("/", 2) + url = f"https://api.github.com/repos/{path_list[0]}/{path_list[1]}" + response = get_github_api_response(url, action_id) + + else: + response = get_github_api_response( + f"https://api.github.com/repos/{path}", action_id + ) + + if response is None: + # Handle github api limit exceed by returning that the action exists without actually checking + # to prevent false errors on linter output. Only show it as an linter error. + return True + + if response.status == 404: + return False + + return True + + +def workflow_files(input: str) -> list: + """ + Takes in an argument of directory and/or files in string format from the CLI. + Returns a sorted set of all workflow files in the path(s) specified. + """ + workflow_files = [] + for path in input.split(): + if os.path.isfile(path): + workflow_files.append(path) + elif os.path.isdir(path): + for subdir, dirs, files in os.walk(path): + for filename in files: + filepath = subdir + os.sep + filename + if filepath.endswith((".yml", ".yaml")): + workflow_files.append(filepath) + + return sorted(set(workflow_files)) + + +def get_action_update(action_id): + """ + Takes in an action id (bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945) + and checks the action repo for the newest version. + If there is a new version, return the url to the updated version. + """ + if "./" in action_id: + # Handle local workflow calls, return None since there will be no updates. + return None + + path, *hash = action_id.split("@") + + if path in memoized_action_update_urls: + return memoized_action_update_urls[path] + else: + if "bitwarden/gh-actions" in path: + path_list = path.split("/", 2) + url = f"https://api.github.com/repos/{path_list[0]}/{path_list[1]}/commits?path={path_list[2]}" + response = get_github_api_response(url, action_id) + if not response: + return None + + sha = json.loads(response.data)[0]["sha"] + if sha not in hash: + update_url = ( + f"https://github.com/{path_list[0]}/{path_list[1]}/commit/{sha}" + ) + memoized_action_update_urls[path] = update_url + return update_url + else: + # Get tag from latest release + response = get_github_api_response( + f"https://api.github.com/repos/{path}/releases/latest", action_id + ) + if not response: + return None + + tag_name = json.loads(response.data)["tag_name"] + + # Get the URL to the commit for the tag + response = get_github_api_response( + f"https://api.github.com/repos/{path}/git/ref/tags/{tag_name}", + action_id, + ) + if not response: + return None + + if json.loads(response.data)["object"]["type"] == "commit": + sha = json.loads(response.data)["object"]["sha"] + else: + url = json.loads(response.data)["object"]["url"] + # Follow the URL and get the commit sha for tags + response = get_github_api_response(url, action_id) + if not response: + return None + + sha = json.loads(response.data)["object"]["sha"] + + if sha not in hash: + update_url = f"https://github.com/{path}/commit/{sha}" + memoized_action_update_urls[path] = update_url + return update_url + + +def lint(filename): + + supported_actions = {"act10ns/slack", "actions/cache", "actions/checkout", "actions/delete-package-versions", "actions/download-artifact", "actions/github-script", "actions/labeler", "actions/setup-dotnet", "actions/setup-java", "actions/setup-node", "actions/setup-python", "actions/stale", "actions/upload-artifact", "android-actions/setup-android", "Asana/create-app-attachment-github-action", "Azure/functions-action", "Azure/get-keyvault-secrets", "Azure/login", "azure/webapps-deploy", "bitwarden/sm-action", "checkmarx/ast-github-action", "chrnorm/deployment-action", "chrnorm/deployment-status", "chromaui/action", "cloudflare/pages-action", "convictional/trigger-workflow-and-wait", "crazy-max/ghaction-import-gpg", "crowdin/github-action", "dawidd6/action-download-artifact", "dawidd6/action-homebrew-bump-formula", "digitalocean/action-doctl", "docker/build-push-action", "docker/setup-buildx-action", "docker/setup-qemu-action", "dorny/test-reporter", "dtolnay/rust-toolchain", "futureware-tech/simulator-action", "hashicorp/setup-packer", "macauley/action-homebrew-bump-cask", "microsoft/setup-msbuild", "ncipollo/release-action", "peter-evans/close-issue", "ruby/setup-ruby", "samuelmeuli/action-snapcraft", "snapcore/action-build", "sonarsource/sonarcloud-github-action", "stackrox/kube-linter-action", "Swatinem/rust-cache", "SwiftDocOrg/github-wiki-publish-action", "SwiftDocOrg/swift-doc", "tj-actions/changed-files", "yogevbd/enforce-label-action"} + + + + findings = [] + max_error_level = 0 + + with open(filename) as file: + workflow = yaml.load(file, Loader=yaml.FullLoader) + + # Check for 'name' key for the workflow. + if "name" not in workflow: + findings.append(LintFinding("Name key missing for workflow.", "warning")) + + # Check for 'name' value to be capitalized in workflow. + elif not workflow["name"][0].isupper(): + findings.append( + LintFinding( + f"Name value for workflow is not capitalized. [{workflow['name']}]", + "warning", + ) + ) + + # Loop through jobs in workflow. + if "jobs" in workflow: + jobs = workflow["jobs"] + for job_key in jobs: + job = jobs[job_key] + + # Make sure runner is using pinned version. + runner = job.get("runs-on", "") + if "-latest" in runner: + findings.append( + LintFinding( + f"Runner version is set to '{runner}', but needs to be pinned to a version.", + "warning", + ) + ) + + # Check for 'name' key for job. + if "name" not in job: + findings.append( + LintFinding( + f"Name key missing for job key '{job_key}'.", "warning" + ) + ) + # Check for 'name' value to be capitalized in job. + elif not job["name"][0].isupper(): + findings.append( + LintFinding( + f"Name value of job key '{job_key}' is not capitalized. [{job['name']}]", + "warning", + ) + ) + + # If the job has environment variables defined, then make sure they start with an underscore. + if "env" in job: + for k in job["env"].keys(): + if k[0] != "_": + findings.append( + LintFinding( + f"Environment variable '{k}' of job key '{job_key}' does not start with an underscore.", + "warning", + ) + ) + + # Loop through steps in job. + steps = job.get("steps", "") + for i, step in enumerate(steps, start=1): + # Check for 'name' key for step. + if "name" not in step: + findings.append( + LintFinding( + f"Name key missing for step {str(i)} of job key '{job_key}'.", + "warning", + ) + ) + # Check for 'name' value to be capitalized in step. + elif not step["name"][0].isupper(): + findings.append( + LintFinding( + f"Name value in step {str(i)} of job key '{job_key}' is not capitalized. [{step['name']}]", + "warning", + ) + ) + + if "uses" in step: + try: + path, hash = step["uses"].split("@") + except ValueError: + logging.info("Skipping local action in workflow.") + break + + # If the step has a 'uses' key, check if actions are in supported actions list and also value hash, except bitwarden actions. + if "bitwarden/gh-actions" not in path: + try: + # Check if actions are in supported actions list. + if path not in supported_actions: + findings.append( + LintFinding( + f"Step {str(i)} of job key '{job_key}' uses an unsupported action: {path}.", + "warning", + ) + ) + # Check to make sure SHA1 hash is 40 characters. + if len(hash) != 40: + findings.append( + LintFinding( + f"Step {str(i)} of job key '{job_key}' does not have a valid action hash. (not 40 characters)", + "error", + ) + ) + + # Attempts to convert the hash to a integer + # which will succeed if all characters are hexadecimal + try: + int(hash, 16) + except ValueError: + findings.append( + LintFinding( + f"Step {str(i)} of job key '{job_key}' does not have a valid action hash. (not all hexadecimal characters)", + "error", + ) + ) + except: + findings.append( + LintFinding( + f"Step {str(i)} of job key '{job_key}' does not have a valid action hash. (missing '@' character)", + "error", + ) + ) + + # If the step has a 'uses' key, check path for external workflow + path_list = path.split("/", 2) + + if "bitwarden/gh-actions" in path and len(path_list) < 3: + findings.append( + LintFinding( + f"Step {str(i)} of job key '{job_key}' does not have a valid action path. (missing name of the repository or workflow)", + "error", + ) + ) + elif len(path_list) < 2: + findings.append( + LintFinding( + f"Step {str(i)} of job key '{job_key}' does not have a valid action path. (missing workflow name or the workflow author)", + "error", + ) + ) + # Check if GitHub repository with action exists + elif not action_repo_exists(step["uses"]): + action_id = step["uses"] + findings.append( + LintFinding( + f"Step {str(i)} of job key '{job_key}' uses an non-existing action: {action_id}.", + "error", + ) + ) + else: + # If the step has a 'uses' key and path is correct, check the action id repo for an update. + update_available = get_action_update(step["uses"]) + if update_available: + findings.append( + LintFinding( + f"Step {str(i)} of job key '{job_key}' uses an outdated action, consider updating it '{update_available}'.", + "warning", + ) + ) + + # If the step has a 'run' key and only has one command, check if it's a single line. + if "run" in step: + if step["run"].count("\n") == 1: + findings.append( + LintFinding( + f"Run in step {str(i)} of job key '{job_key}' should be a single line.", + "error", + ) + ) + + if len(findings) > 0: + print("#", filename) + for finding in findings: + print_finding(finding) + print() + + max_error_level = get_max_error_level(findings) + + return max_error_level + + +def main(input_args=None): + + # Pull the arguments from the command line + if not input_args: + input_args = sys.argv[1:] + + # Read arguments from command line. + parser = argparse.ArgumentParser() + parser.add_argument("input", help="file or directory input") + parser.add_argument( + "-s", + "--strict", + action="store_true", + help="return non-zero exit code on warnings " "as well as errors", + ) + args = parser.parse_args(input_args) + # max_error_level = 0 + + # for filename in input_files: + # prob_level = lint(filename) + # max_error_level = max(max_error_level, prob_level) + input_files = workflow_files(args.input) + if len(input_files) > 0: + prob_levels = list(map(lint, input_files)) + + max_error_level = max(prob_levels) + + if max_error_level == PROBLEM_LEVELS["error"]: + return_code = 2 + elif max_error_level == PROBLEM_LEVELS["warning"]: + return_code = 1 if args.strict else 0 + else: + return_code = 0 + + return return_code + else: + print(f'File(s)/Directory: "{args.input}" does not exist, exiting.') + return -1 + + +if __name__ == "__main__": + return_code = main() + print(memoized_action_update_urls) + sys.exit(return_code) diff --git a/lint-workflow/tests/configs.py b/lint-workflow/tests/configs.py new file mode 100644 index 00000000..eec5f0c7 --- /dev/null +++ b/lint-workflow/tests/configs.py @@ -0,0 +1 @@ +FIXTURES_DIR = "./tests/fixtures" diff --git a/lint-workflow/tests/fixtures/test.yml b/lint-workflow/tests/fixtures/test.yml index cd6b71a2..32c7e35d 100644 --- a/lint-workflow/tests/fixtures/test.yml +++ b/lint-workflow/tests/fixtures/test.yml @@ -36,7 +36,8 @@ jobs: echo "$i=$VALUE" >> $GITHUB_OUTPUT done - - uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea # v1.3.2 + - name: Download translations + uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea # v1.3.2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} @@ -52,4 +53,4 @@ jobs: localization_branch_name: crowdin-auto-sync create_pull_request: true pull_request_title: "Autosync Crowdin Translations" - pull_request_body: "Autosync the updated translations" + pull_request_body: "Autosync the updated translations" \ No newline at end of file diff --git a/lint-workflow/tests/fixtures/test_a.yaml b/lint-workflow/tests/fixtures/test_a.yaml index bd0cfb24..f0494a11 100644 --- a/lint-workflow/tests/fixtures/test_a.yaml +++ b/lint-workflow/tests/fixtures/test_a.yaml @@ -7,7 +7,7 @@ on: jobs: call-workflow: - uses: bitwarden/server/.github/workflows/workflow-linter.yml@master + uses: bitwarden/server/.github/workflows/workflow-linter.yml@main test-normal-action: name: Download Latest diff --git a/lint-workflow/tests/test_action_update.py b/lint-workflow/tests/test_action_update.py new file mode 100644 index 00000000..8121e5ec --- /dev/null +++ b/lint-workflow/tests/test_action_update.py @@ -0,0 +1,18 @@ +import urllib3 as urllib + +from lint import get_action_update, memoized_action_update_urls + +http = urllib.PoolManager() + + +def test_action_update(): + action_id = "actions/checkout@86f86b36ef15e6570752e7175f451a512eac206b" + sub_string = "github.com" + update_url = get_action_update(action_id) + assert str(sub_string) in str(update_url) + + r = http.request("GET", update_url) + + assert r.status == 200 + + assert "actions/checkout" in memoized_action_update_urls diff --git a/lint-workflow/tests/test_lint.py b/lint-workflow/tests/test_lint.py index d9a28e37..ecf84a3e 100644 --- a/lint-workflow/tests/test_lint.py +++ b/lint-workflow/tests/test_lint.py @@ -1,43 +1,20 @@ -"""Test src/lint.py.""" -import pytest +from lint import lint +from .configs import FIXTURES_DIR -from src.lint import LinterCmd -from src.utils import Settings, LintFinding, LintLevels - - -@pytest.fixture(name="settings") -def fixture_settings(): - return Settings() - - -def test_get_max_error_level(settings): - linter = LinterCmd(settings=settings) +def test_lint(capfd): + file_path = f"{FIXTURES_DIR}/test.yml" + lint_output = lint(file_path) + out, err = capfd.readouterr() assert ( - linter.get_max_error_level( - [ - LintFinding(level=LintLevels.WARNING), - LintFinding(level=LintLevels.WARNING), - ] - ) - == 1 + "\x1b[33mwarning\x1b[0m Name value for workflow is not capitalized. [crowdin Pull]" + in out ) - assert ( - linter.get_max_error_level( - [LintFinding(level=LintLevels.ERROR), LintFinding(level=LintLevels.ERROR)] - ) - == 2 + "\x1b[33mwarning\x1b[0m Step 4 of job key 'crowdin-pull' uses an outdated action, consider updating it" + in out ) - assert ( - linter.get_max_error_level( - [ - LintFinding(level=LintLevels.ERROR), - LintFinding(level=LintLevels.ERROR), - LintFinding(level=LintLevels.WARNING), - LintFinding(level=LintLevels.WARNING), - ] - ) - == 2 + "\x1b[31merror\x1b[0m Step 2 of job key 'crowdin-pull' uses an non-existing action: Azure/logi@77f1b2e3fb80c0e8645114159d17008b8a2e475a." + in out ) diff --git a/lint-workflow/tests/test_main.py b/lint-workflow/tests/test_main.py new file mode 100644 index 00000000..36a83fda --- /dev/null +++ b/lint-workflow/tests/test_main.py @@ -0,0 +1,59 @@ +from lint import main +from .configs import FIXTURES_DIR + +# Tests for argparse inputs and outputs using capsys.readouterr() + +FIXTURES_DIR = "./tests/fixtures" + + +def test_main_single_file(capsys): + main([f"{FIXTURES_DIR}/test.yml"]) + captured = capsys.readouterr() + result = captured.out + assert "test.yml" in result + + +def test_main_multiple_files(capsys): + main([f"{FIXTURES_DIR}/test.yml {FIXTURES_DIR}/test-alt.yml"]) + captured = capsys.readouterr() + result = captured.out + assert isinstance(result, str) + assert "test.yml" in result + assert "test-alt.yml" in result + + +def test_main_folder(capsys): + main([f"{FIXTURES_DIR}"]) + captured = capsys.readouterr() + result = captured.out + assert isinstance(result, str) + assert "test.yml" in result + assert "test-alt.yml" in result + + +def test_main_folder_and_files(capsys): + main([f"{FIXTURES_DIR}/test.yml {FIXTURES_DIR}"]) + captured = capsys.readouterr() + result = captured.out + print(result) + + +def test_main_not_found(capsys): + # File that doesn't exist + main(["not-a-real-file.yml"]) + captured = capsys.readouterr() + result = captured.out + assert isinstance(result, str) + assert 'File(s)/Directory: "not-a-real-file.yml" does not exist, exiting.' in result + # Empty string + main([""]) + captured = capsys.readouterr() + result = captured.out + assert isinstance(result, str) + assert 'File(s)/Directory: "" does not exist, exiting.' in result + # Spaces in string + main([" "]) + captured = capsys.readouterr() + result = captured.out + assert isinstance(result, str) + assert 'File(s)/Directory: " " does not exist, exiting.' in result diff --git a/lint-workflow/tests/test_workflow_files.py b/lint-workflow/tests/test_workflow_files.py new file mode 100644 index 00000000..493fd6ca --- /dev/null +++ b/lint-workflow/tests/test_workflow_files.py @@ -0,0 +1,35 @@ +import os + +from lint import workflow_files +from .configs import FIXTURES_DIR + + +def test_workflow_files(): + assert workflow_files("") == [] + assert workflow_files("not-a-real-file.yml") == [] + assert workflow_files(f"{FIXTURES_DIR}/test.yml") == [f"{FIXTURES_DIR}/test.yml"] + # multiple files + assert workflow_files( + f"{FIXTURES_DIR}/test.yml {FIXTURES_DIR}/test-alt.yml" + ) == sorted([f"{FIXTURES_DIR}/test.yml", f"{FIXTURES_DIR}/test-alt.yml"]) + # directory + assert workflow_files(FIXTURES_DIR) == sorted( + set( + [ + f"{FIXTURES_DIR}/{file}" + for file in os.listdir(FIXTURES_DIR) + if file.endswith((".yml", ".yaml")) + ] + ) + ) + # directory and files + assert workflow_files(f"{FIXTURES_DIR}/test.yml {FIXTURES_DIR}") == sorted( + set( + [f"{FIXTURES_DIR}/test.yml"] + + [ + f"{FIXTURES_DIR}/{file}" + for file in os.listdir(FIXTURES_DIR) + if file.endswith((".yml", ".yaml")) + ] + ) + ) From f2631286183200f4f7ca3068d0a83d65fc7ccd6d Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 12 Jan 2024 15:52:21 -0800 Subject: [PATCH 34/57] Fix tests after type fixes. Add tests to CI --- .github/workflows/lint-ci.yml | 8 ++++++++ lint-workflow-v2/src/actions.py | 4 +++- lint-workflow-v2/src/lint.py | 4 +++- lint-workflow-v2/src/load.py | 2 +- lint-workflow-v2/src/utils.py | 6 +----- lint-workflow-v2/tests/test_lint.py | 17 ++++++++++------- lint-workflow-v2/tests/test_utils.py | 4 ++-- 7 files changed, 28 insertions(+), 17 deletions(-) diff --git a/.github/workflows/lint-ci.yml b/.github/workflows/lint-ci.yml index cbc78ed5..9c17bc59 100644 --- a/.github/workflows/lint-ci.yml +++ b/.github/workflows/lint-ci.yml @@ -55,3 +55,11 @@ jobs: - name: Test lint working-directory: lint-workflow-v2 run: pipenv run pytest tests --cov=src + + - name: Check type hinting + working-directory: lint-workflow-v2 + run: pipenv run pytype src + + - name: Simple end-to-end testing + working-directory: lint-workflow-v2 + run: pipenv run python cli.py -v lint --files tests/fixtures/test.yml diff --git a/lint-workflow-v2/src/actions.py b/lint-workflow-v2/src/actions.py index 715c5ea5..0ff30b4b 100644 --- a/lint-workflow-v2/src/actions.py +++ b/lint-workflow-v2/src/actions.py @@ -35,7 +35,9 @@ def __init__(self, settings: Optional[Settings] = None) -> None: self.settings = settings @staticmethod - def extend_parser(subparsers: argparse._SubParsersAction) -> argparse._SubParsersAction: + def extend_parser( + subparsers: argparse._SubParsersAction, + ) -> argparse._SubParsersAction: """Extends the CLI subparser with the options for ActionCmd. Add 'actions add' and 'actions update' to the CLI as sub-commands diff --git a/lint-workflow-v2/src/lint.py b/lint-workflow-v2/src/lint.py index c87cea2f..95025e73 100644 --- a/lint-workflow-v2/src/lint.py +++ b/lint-workflow-v2/src/lint.py @@ -30,7 +30,9 @@ def __init__(self, settings: Optional[Settings] = None) -> None: self.rules = Rules(settings=settings) @staticmethod - def extend_parser(subparsers: argparse._SubParsersAction) -> argparse._SubParsersAction: + def extend_parser( + subparsers: argparse._SubParsersAction, + ) -> argparse._SubParsersAction: """Extends the CLI subparser with the options for LintCmd. Add 'lint' as a sub command along with its options and arguments diff --git a/lint-workflow-v2/src/load.py b/lint-workflow-v2/src/load.py index ecd26a17..8ffa6c51 100644 --- a/lint-workflow-v2/src/load.py +++ b/lint-workflow-v2/src/load.py @@ -75,7 +75,7 @@ def build( cls, filename: Optional[str] = None, workflow: Optional[CommentedMap] = None, - from_file: bool = True + from_file: bool = True, ) -> Workflow: """Build a Workflow from either code or a file. diff --git a/lint-workflow-v2/src/utils.py b/lint-workflow-v2/src/utils.py index e26046cc..e7a6395a 100644 --- a/lint-workflow-v2/src/utils.py +++ b/lint-workflow-v2/src/utils.py @@ -37,11 +37,7 @@ class LintLevels(LintLevel, Enum): class LintFinding: """Represents a problem detected by linting.""" - def __init__( - self, - description: str, - level: LintLevels - ) -> None: + def __init__(self, description: str, level: LintLevels) -> None: self.description = description self.level = level diff --git a/lint-workflow-v2/tests/test_lint.py b/lint-workflow-v2/tests/test_lint.py index d9a28e37..ce4f3ec0 100644 --- a/lint-workflow-v2/tests/test_lint.py +++ b/lint-workflow-v2/tests/test_lint.py @@ -16,8 +16,8 @@ def test_get_max_error_level(settings): assert ( linter.get_max_error_level( [ - LintFinding(level=LintLevels.WARNING), - LintFinding(level=LintLevels.WARNING), + LintFinding(description="", level=LintLevels.WARNING), + LintFinding(description="", level=LintLevels.WARNING), ] ) == 1 @@ -25,7 +25,10 @@ def test_get_max_error_level(settings): assert ( linter.get_max_error_level( - [LintFinding(level=LintLevels.ERROR), LintFinding(level=LintLevels.ERROR)] + [ + LintFinding(description="", level=LintLevels.ERROR), + LintFinding(description="", level=LintLevels.ERROR), + ] ) == 2 ) @@ -33,10 +36,10 @@ def test_get_max_error_level(settings): assert ( linter.get_max_error_level( [ - LintFinding(level=LintLevels.ERROR), - LintFinding(level=LintLevels.ERROR), - LintFinding(level=LintLevels.WARNING), - LintFinding(level=LintLevels.WARNING), + LintFinding(description="", level=LintLevels.ERROR), + LintFinding(description="", level=LintLevels.ERROR), + LintFinding(description="", level=LintLevels.WARNING), + LintFinding(description="", level=LintLevels.WARNING), ] ) == 2 diff --git a/lint-workflow-v2/tests/test_utils.py b/lint-workflow-v2/tests/test_utils.py index 98aab502..d79e12ac 100644 --- a/lint-workflow-v2/tests/test_utils.py +++ b/lint-workflow-v2/tests/test_utils.py @@ -27,8 +27,8 @@ def test_lint_level(): def test_lint_finding(): - warning = LintFinding(level=LintLevels.WARNING) + warning = LintFinding(description="", level=LintLevels.WARNING) assert str(warning) == "\x1b[33mwarning\x1b[0m " - error = LintFinding(level=LintLevels.ERROR) + error = LintFinding(description="", level=LintLevels.ERROR) assert str(error) == "\x1b[31merror\x1b[0m " From 3e18bed767f6671bbebb15ce3699cc5a9f1d2542 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 12 Jan 2024 15:53:28 -0800 Subject: [PATCH 35/57] Add new workflow path to the PR trigger --- .github/workflows/lint-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/lint-ci.yml b/.github/workflows/lint-ci.yml index 9c17bc59..d0ee7646 100644 --- a/.github/workflows/lint-ci.yml +++ b/.github/workflows/lint-ci.yml @@ -6,6 +6,7 @@ on: push: paths: - "lint-workflow/**" + - "lint-workflow-v2/**" workflow_dispatch: {} jobs: From 17361c354c54a10e7d85c9fcc3e173edb71b08f9 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 12 Jan 2024 15:55:44 -0800 Subject: [PATCH 36/57] Update trigger to all pull_request events instead of push events --- .github/workflows/lint-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-ci.yml b/.github/workflows/lint-ci.yml index d0ee7646..96ada720 100644 --- a/.github/workflows/lint-ci.yml +++ b/.github/workflows/lint-ci.yml @@ -3,7 +3,7 @@ name: CI-Lint on: - push: + pull_request: paths: - "lint-workflow/**" - "lint-workflow-v2/**" From f61921a896afdf9389badf8f2656bff65ac51b87 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 12 Jan 2024 15:57:02 -0800 Subject: [PATCH 37/57] Update workflow job names to be more helpful --- .github/workflows/lint-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint-ci.yml b/.github/workflows/lint-ci.yml index 96ada720..de2e5cb9 100644 --- a/.github/workflows/lint-ci.yml +++ b/.github/workflows/lint-ci.yml @@ -11,7 +11,7 @@ on: jobs: ci-lint: - name: CI + name: CI workflow-linter (v1) runs-on: ubuntu-22.04 steps: - name: Checkout @@ -35,7 +35,7 @@ jobs: ci-lint-v2: - name: CI + name: CI workflow-linter (v2) runs-on: ubuntu-22.04 steps: - name: Checkout From 8cf90433815f7624f46dd676d693bef206b8280c Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 12 Jan 2024 15:57:48 -0800 Subject: [PATCH 38/57] Switch back to python 3.9 for v1 linter --- .github/workflows/lint-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-ci.yml b/.github/workflows/lint-ci.yml index de2e5cb9..b38adb29 100644 --- a/.github/workflows/lint-ci.yml +++ b/.github/workflows/lint-ci.yml @@ -20,7 +20,7 @@ jobs: - name: Set up Python uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 with: - python-version: "3.11" + python-version: "3.9" - name: Install dependencies working-directory: lint-workflow From d5b0f77ec05f461757d4563ac98989142a1e2e63 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 12 Jan 2024 16:11:40 -0800 Subject: [PATCH 39/57] Fix edge case where 'env' doesn't exist in a job --- lint-workflow-v2/Session.vim | 202 ++++++++++++++++++ lint-workflow-v2/Taskfile.yml | 4 + .../src/rules/job_environment_prefix.py | 11 +- .../rules/test_job_environment_prefix.py | 35 ++- 4 files changed, 243 insertions(+), 9 deletions(-) create mode 100644 lint-workflow-v2/Session.vim diff --git a/lint-workflow-v2/Session.vim b/lint-workflow-v2/Session.vim new file mode 100644 index 00000000..9b1879f3 --- /dev/null +++ b/lint-workflow-v2/Session.vim @@ -0,0 +1,202 @@ +let SessionLoad = 1 +let s:so_save = &g:so | let s:siso_save = &g:siso | setg so=0 siso=0 | setl so=-1 siso=-1 +let v:this_session=expand(":p") +silent only +silent tabonly +cd ~/bitwarden/official-repos/gh-actions/lint-workflow-v2 +if expand('%') == '' && !&modified && line('$') <= 1 && getline(1) == '' + let s:wipebuf = bufnr('%') +endif +let s:shortmess_save = &shortmess +if &shortmess =~ 'A' + set shortmess=aoOA +else + set shortmess=aoO +endif +badd +33 tests/test_lint.py +badd +33 tests/test_utils.py +badd +38 Taskfile.yml +badd +0 src/rules/job_environment_prefix.py +badd +0 tests/rules/test_job_environment_prefix.py +badd +0 tests/fixtures/test_a.yaml +badd +4 src/load.py +argglobal +%argdel +$argadd tests/test_lint.py +set stal=2 +tabnew +setlocal\ bufhidden=wipe +tabrewind +edit src/rules/job_environment_prefix.py +let s:save_splitbelow = &splitbelow +let s:save_splitright = &splitright +set splitbelow splitright +wincmd _ | wincmd | +vsplit +1wincmd h +wincmd w +let &splitbelow = s:save_splitbelow +let &splitright = s:save_splitright +wincmd t +let s:save_winminheight = &winminheight +let s:save_winminwidth = &winminwidth +set winminheight=0 +set winheight=1 +set winminwidth=0 +set winwidth=1 +exe 'vert 1resize ' . ((&columns * 163 + 163) / 327) +exe 'vert 2resize ' . ((&columns * 163 + 163) / 327) +argglobal +balt tests/rules/test_job_environment_prefix.py +setlocal fdm=manual +setlocal fde=0 +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=0 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +silent! normal! zE +let &fdl = &fdl +let s:l = 70 - ((58 * winheight(0) + 31) / 62) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 70 +normal! 018| +wincmd w +argglobal +if bufexists(fnamemodify("tests/rules/test_job_environment_prefix.py", ":p")) | buffer tests/rules/test_job_environment_prefix.py | else | edit tests/rules/test_job_environment_prefix.py | endif +if &buftype ==# 'terminal' + silent file tests/rules/test_job_environment_prefix.py +endif +balt src/rules/job_environment_prefix.py +setlocal fdm=manual +setlocal fde=0 +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=0 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +silent! normal! zE +let &fdl = &fdl +let s:l = 40 - ((32 * winheight(0) + 31) / 62) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 40 +normal! 0 +wincmd w +exe 'vert 1resize ' . ((&columns * 163 + 163) / 327) +exe 'vert 2resize ' . ((&columns * 163 + 163) / 327) +tabnext +edit src/load.py +let s:save_splitbelow = &splitbelow +let s:save_splitright = &splitright +set splitbelow splitright +wincmd _ | wincmd | +vsplit +wincmd _ | wincmd | +vsplit +2wincmd h +wincmd w +wincmd w +let &splitbelow = s:save_splitbelow +let &splitright = s:save_splitright +wincmd t +let s:save_winminheight = &winminheight +let s:save_winminwidth = &winminwidth +set winminheight=0 +set winheight=1 +set winminwidth=0 +set winwidth=1 +exe 'vert 1resize ' . ((&columns * 109 + 163) / 327) +exe 'vert 2resize ' . ((&columns * 108 + 163) / 327) +exe 'vert 3resize ' . ((&columns * 108 + 163) / 327) +argglobal +balt tests/fixtures/test_a.yaml +setlocal fdm=manual +setlocal fde=0 +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=0 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +silent! normal! zE +let &fdl = &fdl +let s:l = 60 - ((26 * winheight(0) + 30) / 61) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 60 +normal! 0 +wincmd w +argglobal +if bufexists(fnamemodify("tests/fixtures/test_a.yaml", ":p")) | buffer tests/fixtures/test_a.yaml | else | edit tests/fixtures/test_a.yaml | endif +if &buftype ==# 'terminal' + silent file tests/fixtures/test_a.yaml +endif +balt src/load.py +setlocal fdm=manual +setlocal fde=0 +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=0 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +silent! normal! zE +let &fdl = &fdl +let s:l = 8 - ((7 * winheight(0) + 30) / 61) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 8 +normal! 0 +wincmd w +argglobal +if bufexists(fnamemodify("Taskfile.yml", ":p")) | buffer Taskfile.yml | else | edit Taskfile.yml | endif +if &buftype ==# 'terminal' + silent file Taskfile.yml +endif +balt tests/fixtures/test_a.yaml +setlocal fdm=manual +setlocal fde=0 +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=0 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +silent! normal! zE +let &fdl = &fdl +let s:l = 39 - ((38 * winheight(0) + 30) / 61) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 39 +normal! 063| +wincmd w +exe 'vert 1resize ' . ((&columns * 109 + 163) / 327) +exe 'vert 2resize ' . ((&columns * 108 + 163) / 327) +exe 'vert 3resize ' . ((&columns * 108 + 163) / 327) +tabnext 2 +set stal=1 +if exists('s:wipebuf') && len(win_findbuf(s:wipebuf)) == 0 && getbufvar(s:wipebuf, '&buftype') isnot# 'terminal' + silent exe 'bwipe ' . s:wipebuf +endif +unlet! s:wipebuf +set winheight=1 winwidth=20 +let &shortmess = s:shortmess_save +let &winminheight = s:save_winminheight +let &winminwidth = s:save_winminwidth +let s:sx = expand(":p:r")."x.vim" +if filereadable(s:sx) + exe "source " . fnameescape(s:sx) +endif +let &g:so = s:so_save | let &g:siso = s:siso_save +set hlsearch +doautoall SessionLoadPost +unlet SessionLoad +" vim: set ft=vim : diff --git a/lint-workflow-v2/Taskfile.yml b/lint-workflow-v2/Taskfile.yml index 98dea17d..c5b1afeb 100644 --- a/lint-workflow-v2/Taskfile.yml +++ b/lint-workflow-v2/Taskfile.yml @@ -35,6 +35,10 @@ tasks: - pipenv run pytest --cov-report term-missing --cov=src tests test:e2e:lint: + cmds: + - pipenv run python cli.py -v lint --files tests/fixtures + + test:e2e:lint:single: cmds: - pipenv run python cli.py -v lint --files tests/fixtures/test.yml diff --git a/lint-workflow-v2/src/rules/job_environment_prefix.py b/lint-workflow-v2/src/rules/job_environment_prefix.py index 5f81e50b..ed801091 100644 --- a/lint-workflow-v2/src/rules/job_environment_prefix.py +++ b/lint-workflow-v2/src/rules/job_environment_prefix.py @@ -60,11 +60,12 @@ def fn(self, obj: Job) -> Tuple[bool, str]: """ correct = True - offending_keys = [] - for key in obj.env.keys(): - if key[0] != "_": - offending_keys.append(key) - correct = False + if obj.env: + offending_keys = [] + for key in obj.env.keys(): + if key[0] != "_": + offending_keys.append(key) + correct = False if correct: return True, "" diff --git a/lint-workflow-v2/tests/rules/test_job_environment_prefix.py b/lint-workflow-v2/tests/rules/test_job_environment_prefix.py index 25f03c42..832d9713 100644 --- a/lint-workflow-v2/tests/rules/test_job_environment_prefix.py +++ b/lint-workflow-v2/tests/rules/test_job_environment_prefix.py @@ -27,8 +27,24 @@ def fixture_correct_workflow(): return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) -@pytest.fixture(name="incorrect_workflow") -def fixture_incorrect_workflow(): +@pytest.fixture(name="no_env_workflow") +def fixture_no_env_workflow(): + workflow = """\ +--- +on: + workflow_dispatch: + +jobs: + job-key: + runs-on: ubuntu-22.04 + steps: + - run: echo test +""" + return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) + + +@pytest.fixture(name="missing_prefix_workflow") +def fixture_missing_prefix_workflow(): workflow = """\ --- on: @@ -61,8 +77,19 @@ def test_rule_on_correct_workflow(rule, correct_workflow): assert finding is None -def test_rule_on_incorrect_workflow(rule, incorrect_workflow): - obj = incorrect_workflow.jobs["job-key"] +def test_rule_on_no_env_workflow(rule, no_env_workflow): + obj = no_env_workflow.jobs["job-key"] + + result, message = rule.fn(no_env_workflow.jobs["job-key"]) + assert result is True + assert message == "" + + finding = rule.execute(obj) + assert finding is None + + +def test_rule_on_missing_prefix_workflow(rule, missing_prefix_workflow): + obj = missing_prefix_workflow.jobs["job-key"] result, message = rule.fn(obj) assert result is False From a957c844e1ec4984d966b90ecd4aec552f29c3b7 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 26 Jan 2024 10:02:12 -0800 Subject: [PATCH 40/57] Remove the end-to-end testing since it fails (as expected) and prevents a successful CI run --- .github/workflows/lint-ci.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/lint-ci.yml b/.github/workflows/lint-ci.yml index b38adb29..ee3f0eb3 100644 --- a/.github/workflows/lint-ci.yml +++ b/.github/workflows/lint-ci.yml @@ -60,7 +60,3 @@ jobs: - name: Check type hinting working-directory: lint-workflow-v2 run: pipenv run pytype src - - - name: Simple end-to-end testing - working-directory: lint-workflow-v2 - run: pipenv run python cli.py -v lint --files tests/fixtures/test.yml From e133520058ef69dcdb77e8ad11204372af158abf Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 26 Jan 2024 15:32:13 -0800 Subject: [PATCH 41/57] Working on packaging with Hatch --- lint-workflow-v2/.gitignore | 1 + lint-workflow-v2/Pipfile | 2 + lint-workflow-v2/Pipfile.lock | 350 ++++++++++-------- lint-workflow-v2/README.md | 40 +- lint-workflow-v2/Taskfile.yml | 10 + .../{actions.json => actions.json.bak} | 0 lint-workflow-v2/flake.nix | 4 +- lint-workflow-v2/pyproject.toml | 29 ++ lint-workflow-v2/settings.py | 7 +- lint-workflow-v2/src/__about__.py | 1 + lint-workflow-v2/src/actions.py | 1 + lint-workflow-v2/{ => src}/cli.py | 22 +- lint-workflow-v2/src/default_actions.json | 262 +++++++++++++ lint-workflow-v2/src/default_settings.py | 20 + lint-workflow-v2/src/lint.py | 1 + lint-workflow-v2/src/load.py | 1 + lint-workflow-v2/src/models/job.py | 1 + lint-workflow-v2/src/models/step.py | 1 + lint-workflow-v2/src/models/workflow.py | 1 + lint-workflow-v2/src/rule.py | 1 + .../src/rules/job_environment_prefix.py | 1 + .../src/rules/name_capitalized.py | 1 + lint-workflow-v2/src/rules/name_exists.py | 1 + .../src/rules/pinned_job_runner.py | 1 + lint-workflow-v2/src/rules/step_approved.py | 1 + lint-workflow-v2/src/rules/step_pinned.py | 1 + lint-workflow-v2/src/utils.py | 35 ++ .../rules/test_job_environment_prefix.py | 1 + .../tests/rules/test_name_capitalized.py | 1 + .../tests/rules/test_name_exists.py | 1 + .../tests/rules/test_pinned_job_runner.py | 1 + .../tests/rules/test_step_approved.py | 1 + .../tests/rules/test_step_pinned.py | 1 + lint-workflow-v2/tests/test_job.py | 1 + lint-workflow-v2/tests/test_lint.py | 1 + lint-workflow-v2/tests/test_load.py | 1 + lint-workflow-v2/tests/test_rule.py | 1 + lint-workflow-v2/tests/test_step.py | 1 + lint-workflow-v2/tests/test_utils.py | 1 + lint-workflow-v2/tests/test_workflow.py | 1 + 40 files changed, 630 insertions(+), 179 deletions(-) rename lint-workflow-v2/{actions.json => actions.json.bak} (100%) create mode 100644 lint-workflow-v2/pyproject.toml create mode 100644 lint-workflow-v2/src/__about__.py rename lint-workflow-v2/{ => src}/cli.py (72%) create mode 100644 lint-workflow-v2/src/default_actions.json create mode 100644 lint-workflow-v2/src/default_settings.py diff --git a/lint-workflow-v2/.gitignore b/lint-workflow-v2/.gitignore index 6350e986..40575ed1 100644 --- a/lint-workflow-v2/.gitignore +++ b/lint-workflow-v2/.gitignore @@ -1 +1,2 @@ .coverage +dist diff --git a/lint-workflow-v2/Pipfile b/lint-workflow-v2/Pipfile index 8f75f162..8dc6f95b 100644 --- a/lint-workflow-v2/Pipfile +++ b/lint-workflow-v2/Pipfile @@ -17,6 +17,8 @@ coverage = "*" pytest-cov = "*" pylint = "*" pytype = "*" +hatchling = "*" +build = "*" [requires] python_version = "3.11" diff --git a/lint-workflow-v2/Pipfile.lock b/lint-workflow-v2/Pipfile.lock index d891658d..99f0822d 100644 --- a/lint-workflow-v2/Pipfile.lock +++ b/lint-workflow-v2/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "8b34ae1118eb5c9fac2c552c644f66f9dbd05415e08adf78efb99ba8a7f072f3" + "sha256": "7e51933f1987a5e1d1182f4cec2a1c6f84194a17ac6653bb3674da0fe26dfb7f" }, "pipfile-spec": 6, "requires": { @@ -206,6 +206,7 @@ "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", + "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", @@ -338,31 +339,39 @@ }, "black": { "hashes": [ - "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50", - "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f", - "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e", - "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec", - "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055", - "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3", - "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5", - "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54", - "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b", - "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e", - "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e", - "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba", - "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea", - "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59", - "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d", - "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0", - "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9", - "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a", - "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e", - "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba", - "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2", - "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2" + "sha256:0cd59d01bf3306ff7e3076dd7f4435fcd2fafe5506a6111cae1138fc7de52382", + "sha256:1e0fa70b8464055069864a4733901b31cbdbe1273f63a24d2fa9d726723d45ac", + "sha256:30fbf768cd4f4576598b1db0202413fafea9a227ef808d1a12230c643cefe9fc", + "sha256:39addf23f7070dbc0b5518cdb2018468ac249d7412a669b50ccca18427dba1f3", + "sha256:5134a6f6b683aa0a5592e3fd61dd3519d8acd953d93e2b8b76f9981245b65594", + "sha256:6a8977774929b5db90442729f131221e58cc5d8208023c6af9110f26f75b6b20", + "sha256:6cc5a6ba3e671cfea95a40030b16a98ee7dc2e22b6427a6f3389567ecf1b5262", + "sha256:780f13d03066a7daf1707ec723fdb36bd698ffa29d95a2e7ef33a8dd8fe43b5c", + "sha256:7fa8d9aaa22d846f8c0f7f07391148e5e346562e9b215794f9101a8339d8b6d8", + "sha256:827a7c0da520dd2f8e6d7d3595f4591aa62ccccce95b16c0e94bb4066374c4c2", + "sha256:82d9452aeabd51d1c8f0d52d4d18e82b9f010ecb30fd55867b5ff95904f427ff", + "sha256:94d5280d020dadfafc75d7cae899609ed38653d3f5e82e7ce58f75e76387ed3d", + "sha256:9aede09f72b2a466e673ee9fca96e4bccc36f463cac28a35ce741f0fd13aea8b", + "sha256:a15670c650668399c4b5eae32e222728185961d6ef6b568f62c1681d57b381ba", + "sha256:a5a0100b4bdb3744dd68412c3789f472d822dc058bb3857743342f8d7f93a5a7", + "sha256:aaf9aa85aaaa466bf969e7dd259547f4481b712fe7ee14befeecc152c403ee05", + "sha256:be305563ff4a2dea813f699daaffac60b977935f3264f66922b1936a5e492ee4", + "sha256:bf8dd261ee82df1abfb591f97e174345ab7375a55019cc93ad38993b9ff5c6ad", + "sha256:d74d4d0da276fbe3b95aa1f404182562c28a04402e4ece60cf373d0b902f33a0", + "sha256:e0e367759062dcabcd9a426d12450c6d61faf1704a352a49055a04c9f9ce8f5a", + "sha256:ec489cae76eac3f7573629955573c3a0e913641cafb9e3bfc87d8ce155ebdb29", + "sha256:f0dfbfbacfbf9cd1fac7a5ddd3e72510ffa93e841a69fcf4a6358feab1685382" ], "index": "pypi", - "version": "==23.12.1" + "version": "==24.1.0" + }, + "build": { + "hashes": [ + "sha256:538aab1b64f9828977f84bc63ae570b060a8ed1be419e7870b8b4fc5e6ea553b", + "sha256:589bf99a67df7c9cf07ec0ac0e5e2ea5d4b37ac63301c4986d1acb126aa83f8f" + ], + "index": "pypi", + "version": "==1.0.3" }, "click": { "hashes": [ @@ -374,61 +383,61 @@ }, "coverage": { "hashes": [ - "sha256:04387a4a6ecb330c1878907ce0dc04078ea72a869263e53c72a1ba5bbdf380ca", - "sha256:0676cd0ba581e514b7f726495ea75aba3eb20899d824636c6f59b0ed2f88c471", - "sha256:0e8d06778e8fbffccfe96331a3946237f87b1e1d359d7fbe8b06b96c95a5407a", - "sha256:0eb3c2f32dabe3a4aaf6441dde94f35687224dfd7eb2a7f47f3fd9428e421058", - "sha256:109f5985182b6b81fe33323ab4707011875198c41964f014579cf82cebf2bb85", - "sha256:13eaf476ec3e883fe3e5fe3707caeb88268a06284484a3daf8250259ef1ba143", - "sha256:164fdcc3246c69a6526a59b744b62e303039a81e42cfbbdc171c91a8cc2f9446", - "sha256:26776ff6c711d9d835557ee453082025d871e30b3fd6c27fcef14733f67f0590", - "sha256:26f66da8695719ccf90e794ed567a1549bb2644a706b41e9f6eae6816b398c4a", - "sha256:29f3abe810930311c0b5d1a7140f6395369c3db1be68345638c33eec07535105", - "sha256:316543f71025a6565677d84bc4df2114e9b6a615aa39fb165d697dba06a54af9", - "sha256:36b0ea8ab20d6a7564e89cb6135920bc9188fb5f1f7152e94e8300b7b189441a", - "sha256:3cc9d4bc55de8003663ec94c2f215d12d42ceea128da8f0f4036235a119c88ac", - "sha256:485e9f897cf4856a65a57c7f6ea3dc0d4e6c076c87311d4bc003f82cfe199d25", - "sha256:5040148f4ec43644702e7b16ca864c5314ccb8ee0751ef617d49aa0e2d6bf4f2", - "sha256:51456e6fa099a8d9d91497202d9563a320513fcf59f33991b0661a4a6f2ad450", - "sha256:53d7d9158ee03956e0eadac38dfa1ec8068431ef8058fe6447043db1fb40d932", - "sha256:5a10a4920def78bbfff4eff8a05c51be03e42f1c3735be42d851f199144897ba", - "sha256:5b14b4f8760006bfdb6e08667af7bc2d8d9bfdb648351915315ea17645347137", - "sha256:5b2ccb7548a0b65974860a78c9ffe1173cfb5877460e5a229238d985565574ae", - "sha256:697d1317e5290a313ef0d369650cfee1a114abb6021fa239ca12b4849ebbd614", - "sha256:6ae8c9d301207e6856865867d762a4b6fd379c714fcc0607a84b92ee63feff70", - "sha256:707c0f58cb1712b8809ece32b68996ee1e609f71bd14615bd8f87a1293cb610e", - "sha256:74775198b702868ec2d058cb92720a3c5a9177296f75bd97317c787daf711505", - "sha256:756ded44f47f330666843b5781be126ab57bb57c22adbb07d83f6b519783b870", - "sha256:76f03940f9973bfaee8cfba70ac991825611b9aac047e5c80d499a44079ec0bc", - "sha256:79287fd95585ed36e83182794a57a46aeae0b64ca53929d1176db56aacc83451", - "sha256:799c8f873794a08cdf216aa5d0531c6a3747793b70c53f70e98259720a6fe2d7", - "sha256:7d360587e64d006402b7116623cebf9d48893329ef035278969fa3bbf75b697e", - "sha256:80b5ee39b7f0131ebec7968baa9b2309eddb35b8403d1869e08f024efd883566", - "sha256:815ac2d0f3398a14286dc2cea223a6f338109f9ecf39a71160cd1628786bc6f5", - "sha256:83c2dda2666fe32332f8e87481eed056c8b4d163fe18ecc690b02802d36a4d26", - "sha256:846f52f46e212affb5bcf131c952fb4075b55aae6b61adc9856222df89cbe3e2", - "sha256:936d38794044b26c99d3dd004d8af0035ac535b92090f7f2bb5aa9c8e2f5cd42", - "sha256:9864463c1c2f9cb3b5db2cf1ff475eed2f0b4285c2aaf4d357b69959941aa555", - "sha256:995ea5c48c4ebfd898eacb098164b3cc826ba273b3049e4a889658548e321b43", - "sha256:a1526d265743fb49363974b7aa8d5899ff64ee07df47dd8d3e37dcc0818f09ed", - "sha256:a56de34db7b7ff77056a37aedded01b2b98b508227d2d0979d373a9b5d353daa", - "sha256:a7c97726520f784239f6c62506bc70e48d01ae71e9da128259d61ca5e9788516", - "sha256:b8e99f06160602bc64da35158bb76c73522a4010f0649be44a4e167ff8555952", - "sha256:bb1de682da0b824411e00a0d4da5a784ec6496b6850fdf8c865c1d68c0e318dd", - "sha256:bf477c355274a72435ceb140dc42de0dc1e1e0bf6e97195be30487d8eaaf1a09", - "sha256:bf635a52fc1ea401baf88843ae8708591aa4adff875e5c23220de43b1ccf575c", - "sha256:bfd5db349d15c08311702611f3dccbef4b4e2ec148fcc636cf8739519b4a5c0f", - "sha256:c530833afc4707fe48524a44844493f36d8727f04dcce91fb978c414a8556cc6", - "sha256:cc6d65b21c219ec2072c1293c505cf36e4e913a3f936d80028993dd73c7906b1", - "sha256:cd3c1e4cb2ff0083758f09be0f77402e1bdf704adb7f89108007300a6da587d0", - "sha256:cfd2a8b6b0d8e66e944d47cdec2f47c48fef2ba2f2dff5a9a75757f64172857e", - "sha256:d0ca5c71a5a1765a0f8f88022c52b6b8be740e512980362f7fdbb03725a0d6b9", - "sha256:e7defbb9737274023e2d7af02cac77043c86ce88a907c58f42b580a97d5bcca9", - "sha256:e9d1bf53c4c8de58d22e0e956a79a5b37f754ed1ffdbf1a260d9dcfa2d8a325e", - "sha256:ea81d8f9691bb53f4fb4db603203029643caffc82bf998ab5b59ca05560f4c06" + "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61", + "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1", + "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7", + "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7", + "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75", + "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd", + "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35", + "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04", + "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6", + "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042", + "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166", + "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1", + "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d", + "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c", + "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66", + "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70", + "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1", + "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676", + "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630", + "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a", + "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74", + "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad", + "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19", + "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6", + "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448", + "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018", + "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218", + "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756", + "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54", + "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45", + "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628", + "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968", + "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d", + "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25", + "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60", + "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950", + "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06", + "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295", + "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b", + "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c", + "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc", + "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74", + "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1", + "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee", + "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011", + "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156", + "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766", + "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5", + "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581", + "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016", + "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c", + "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3" ], "index": "pypi", - "version": "==7.4.0" + "version": "==7.4.1" }, "dill": { "hashes": [ @@ -438,6 +447,22 @@ "markers": "python_version >= '3.11'", "version": "==0.3.7" }, + "editables": { + "hashes": [ + "sha256:309627d9b5c4adc0e668d8c6fa7bac1ba7c8c5d415c2d27f60f081f8e80d1de2", + "sha256:61e5ffa82629e0d8bfe09bc44a07db3c1ab8ed1ce78a6980732870f19b5e7d4c" + ], + "markers": "python_version >= '3.7'", + "version": "==0.5" + }, + "hatchling": { + "hashes": [ + "sha256:21e8c13f8458b219a91cb84e5b61c15bf786695d1c4fabc29e91e78f94bfe892", + "sha256:bba440453a224e7d4478457fa2e8d8c3633765bafa02975a6b53b9bf917980bc" + ], + "index": "pypi", + "version": "==1.21.1" + }, "importlab": { "hashes": [ "sha256:124cfa00e8a34fefe8aac1a5e94f56c781b178c9eb61a1d3f60f7e03b77338d3", @@ -509,69 +534,69 @@ }, "markupsafe": { "hashes": [ - "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e", - "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e", - "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431", - "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686", - "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c", - "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559", - "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc", - "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb", - "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939", - "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c", - "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0", - "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4", - "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9", - "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575", - "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba", - "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d", - "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd", - "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3", - "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00", - "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155", - "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac", - "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52", - "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f", - "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8", - "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b", - "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007", - "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24", - "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea", - "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198", - "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0", - "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee", - "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be", - "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2", - "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1", - "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707", - "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6", - "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c", - "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58", - "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823", - "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779", - "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636", - "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c", - "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad", - "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee", - "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc", - "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2", - "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48", - "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7", - "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e", - "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b", - "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa", - "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5", - "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e", - "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb", - "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9", - "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57", - "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc", - "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc", - "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2", - "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11" + "sha256:0042d6a9880b38e1dd9ff83146cc3c9c18a059b9360ceae207805567aacccc69", + "sha256:0c26f67b3fe27302d3a412b85ef696792c4a2386293c53ba683a89562f9399b0", + "sha256:0fbad3d346df8f9d72622ac71b69565e621ada2ce6572f37c2eae8dacd60385d", + "sha256:15866d7f2dc60cfdde12ebb4e75e41be862348b4728300c36cdf405e258415ec", + "sha256:1c98c33ffe20e9a489145d97070a435ea0679fddaabcafe19982fe9c971987d5", + "sha256:21e7af8091007bf4bebf4521184f4880a6acab8df0df52ef9e513d8e5db23411", + "sha256:23984d1bdae01bee794267424af55eef4dfc038dc5d1272860669b2aa025c9e3", + "sha256:31f57d64c336b8ccb1966d156932f3daa4fee74176b0fdc48ef580be774aae74", + "sha256:3583a3a3ab7958e354dc1d25be74aee6228938312ee875a22330c4dc2e41beb0", + "sha256:36d7626a8cca4d34216875aee5a1d3d654bb3dac201c1c003d182283e3205949", + "sha256:396549cea79e8ca4ba65525470d534e8a41070e6b3500ce2414921099cb73e8d", + "sha256:3a66c36a3864df95e4f62f9167c734b3b1192cb0851b43d7cc08040c074c6279", + "sha256:3aae9af4cac263007fd6309c64c6ab4506dd2b79382d9d19a1994f9240b8db4f", + "sha256:3ab3a886a237f6e9c9f4f7d272067e712cdb4efa774bef494dccad08f39d8ae6", + "sha256:47bb5f0142b8b64ed1399b6b60f700a580335c8e1c57f2f15587bd072012decc", + "sha256:49a3b78a5af63ec10d8604180380c13dcd870aba7928c1fe04e881d5c792dc4e", + "sha256:4df98d4a9cd6a88d6a585852f56f2155c9cdb6aec78361a19f938810aa020954", + "sha256:5045e892cfdaecc5b4c01822f353cf2c8feb88a6ec1c0adef2a2e705eef0f656", + "sha256:5244324676254697fe5c181fc762284e2c5fceeb1c4e3e7f6aca2b6f107e60dc", + "sha256:54635102ba3cf5da26eb6f96c4b8c53af8a9c0d97b64bdcb592596a6255d8518", + "sha256:54a7e1380dfece8847c71bf7e33da5d084e9b889c75eca19100ef98027bd9f56", + "sha256:55d03fea4c4e9fd0ad75dc2e7e2b6757b80c152c032ea1d1de487461d8140efc", + "sha256:698e84142f3f884114ea8cf83e7a67ca8f4ace8454e78fe960646c6c91c63bfa", + "sha256:6aa5e2e7fc9bc042ae82d8b79d795b9a62bd8f15ba1e7594e3db243f158b5565", + "sha256:7653fa39578957bc42e5ebc15cf4361d9e0ee4b702d7d5ec96cdac860953c5b4", + "sha256:765f036a3d00395a326df2835d8f86b637dbaf9832f90f5d196c3b8a7a5080cb", + "sha256:78bc995e004681246e85e28e068111a4c3f35f34e6c62da1471e844ee1446250", + "sha256:7a07f40ef8f0fbc5ef1000d0c78771f4d5ca03b4953fc162749772916b298fc4", + "sha256:8b570a1537367b52396e53325769608f2a687ec9a4363647af1cded8928af959", + "sha256:987d13fe1d23e12a66ca2073b8d2e2a75cec2ecb8eab43ff5624ba0ad42764bc", + "sha256:9896fca4a8eb246defc8b2a7ac77ef7553b638e04fbf170bff78a40fa8a91474", + "sha256:9e9e3c4020aa2dc62d5dd6743a69e399ce3de58320522948af6140ac959ab863", + "sha256:a0b838c37ba596fcbfca71651a104a611543077156cb0a26fe0c475e1f152ee8", + "sha256:a4d176cfdfde84f732c4a53109b293d05883e952bbba68b857ae446fa3119b4f", + "sha256:a76055d5cb1c23485d7ddae533229039b850db711c554a12ea64a0fd8a0129e2", + "sha256:a76cd37d229fc385738bd1ce4cba2a121cf26b53864c1772694ad0ad348e509e", + "sha256:a7cc49ef48a3c7a0005a949f3c04f8baa5409d3f663a1b36f0eba9bfe2a0396e", + "sha256:abf5ebbec056817057bfafc0445916bb688a255a5146f900445d081db08cbabb", + "sha256:b0fe73bac2fed83839dbdbe6da84ae2a31c11cfc1c777a40dbd8ac8a6ed1560f", + "sha256:b6f14a9cd50c3cb100eb94b3273131c80d102e19bb20253ac7bd7336118a673a", + "sha256:b83041cda633871572f0d3c41dddd5582ad7d22f65a72eacd8d3d6d00291df26", + "sha256:b835aba863195269ea358cecc21b400276747cc977492319fd7682b8cd2c253d", + "sha256:bf1196dcc239e608605b716e7b166eb5faf4bc192f8a44b81e85251e62584bd2", + "sha256:c669391319973e49a7c6230c218a1e3044710bc1ce4c8e6eb71f7e6d43a2c131", + "sha256:c7556bafeaa0a50e2fe7dc86e0382dea349ebcad8f010d5a7dc6ba568eaaa789", + "sha256:c8f253a84dbd2c63c19590fa86a032ef3d8cc18923b8049d91bcdeeb2581fbf6", + "sha256:d18b66fe626ac412d96c2ab536306c736c66cf2a31c243a45025156cc190dc8a", + "sha256:d5291d98cd3ad9a562883468c690a2a238c4a6388ab3bd155b0c75dd55ece858", + "sha256:d5c31fe855c77cad679b302aabc42d724ed87c043b1432d457f4976add1c2c3e", + "sha256:d6e427c7378c7f1b2bef6a344c925b8b63623d3321c09a237b7cc0e77dd98ceb", + "sha256:dac1ebf6983148b45b5fa48593950f90ed6d1d26300604f321c74a9ca1609f8e", + "sha256:de8153a7aae3835484ac168a9a9bdaa0c5eee4e0bc595503c95d53b942879c84", + "sha256:e1a0d1924a5013d4f294087e00024ad25668234569289650929ab871231668e7", + "sha256:e7902211afd0af05fbadcc9a312e4cf10f27b779cf1323e78d52377ae4b72bea", + "sha256:e888ff76ceb39601c59e219f281466c6d7e66bd375b4ec1ce83bcdc68306796b", + "sha256:f06e5a9e99b7df44640767842f414ed5d7bedaaa78cd817ce04bbd6fd86e2dd6", + "sha256:f6be2d708a9d0e9b0054856f07ac7070fbe1754be40ca8525d5adccdbda8f475", + "sha256:f9917691f410a2e0897d1ef99619fd3f7dd503647c8ff2475bf90c3cf222ad74", + "sha256:fc1a75aa8f11b87910ffd98de62b29d6520b6d6e8a3de69a70ca34dea85d2a8a", + "sha256:fe8512ed897d5daf089e5bd010c3dc03bb1bdae00b35588c49b98268d4a01e00" ], "markers": "python_version >= '3.7'", - "version": "==2.1.3" + "version": "==2.1.4" }, "mccabe": { "hashes": [ @@ -643,11 +668,11 @@ }, "pluggy": { "hashes": [ - "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12", - "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7" + "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981", + "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be" ], "markers": "python_version >= '3.8'", - "version": "==1.3.0" + "version": "==1.4.0" }, "pycnite": { "hashes": [ @@ -681,6 +706,14 @@ "markers": "python_full_version >= '3.6.8'", "version": "==3.1.1" }, + "pyproject-hooks": { + "hashes": [ + "sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8", + "sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5" + ], + "markers": "python_version >= '3.7'", + "version": "==1.0.0" + }, "pytest": { "hashes": [ "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", @@ -699,21 +732,22 @@ }, "pytype": { "hashes": [ - "sha256:0e59fb2866cdd804b31ef9baa0f53bc6fddaeaf68a7b1af6aa62c1b9854f017e", - "sha256:144e83bb7b80e5b0972a25bed2e4f03c0cd91d677ad0902bb775fdac156151ca", - "sha256:20680085e5a7beee2aaedddbf96863efa96ff047061ae82e31404336180502f5", - "sha256:33227cd847df1c5e92dbe013dc3926b78c77b36032a1caa3e488286aa9a0a14e", - "sha256:5281cc89ba5acc5a9184845f5c02319c0fbfcd87a9ab4920b6bce0d0caec6860", - "sha256:734d34b3ce13ccea64c419f88630fd189d6d06cbb24516cfb6fe36edc1860da2", - "sha256:a964a105af46fff3495be76ee34ad34acf47300018244122ea75d0e777d04cb5", - "sha256:af4b7ced6049e7fececb646262a25874e8a4e6a0b7e540c100b673aacd230ffa", - "sha256:b1b802867ede7cfd7dbe479cfdd3a1341dac005bfcb2718ff22070494fdd57be", - "sha256:b1c5857acd6348e9f5ace6427d74824f433d6bbfad0e0104b98f5078f5781af6", - "sha256:bf28233b140e9a7702cffcc8346c8d47b492813bee5f11ba2cef906ff4c05c55", - "sha256:c22db76f45a218c673f70c4ab32e7de757b808ce5d9ae55d1b3621f05187c496" + "sha256:0339eb1dc696ab253bb627cd3673f1ad3f485e50103240edc36793ab89465193", + "sha256:1d87b5b4a931d62b4a7ea43f7be340dcb7bc2c3b27889e75f609e434cbc7b5f9", + "sha256:20c22a482af5dc338a52b9d1b461062fd354b54d5457e381550196b6f32141bb", + "sha256:28ab1536f5abe05c704e872c03df1d3a875c1775a5efe94f54f83cf07a4124bc", + "sha256:2cbaa77cd20bc51c73d0558aba9cea0a3cc43502e5a70cc692ad4272d15b338c", + "sha256:888d2fda3a4b6b2c427ab589a016939df3ae4c7139dbef5c022337b44b9e27b5", + "sha256:906b88fd475817783f1998afedd3cd269416920fdf86166a4c6d8c87b90da8e3", + "sha256:9324778305cf869e0d8379d1f8d549f85aed215fdad33023b80be0fc77decd97", + "sha256:aa62ca9890069f87a292a95816bea1ff1ac789f945a3230d6b2f03e2d90a9bb0", + "sha256:bcd0f87ab381f3a1f3d0cac90524ac6bf7f43d9c949b59db46031072cbbafe6c", + "sha256:ca4624812e07610654a9052c515f7a3de44c44a6fb3be37df9746561d7465b72", + "sha256:d01e4b210154e6d62eeb09837e5512cc3a47cb3108dcb8484f6799fb03e91ba9", + "sha256:f544a252363e93d64f7716100a251b0cbea906cac88518ba5fc29d55eec1c1d6" ], "index": "pypi", - "version": "==2024.1.5" + "version": "==2024.1.24" }, "pyyaml": { "hashes": [ @@ -746,6 +780,7 @@ "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", + "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", @@ -795,6 +830,13 @@ "markers": "python_version >= '3.7'", "version": "==0.12.3" }, + "trove-classifiers": { + "hashes": [ + "sha256:3c1ff4deb10149c7e39ede6e5bbc107def64362ef1ee7590ec98d71fb92f1b6a", + "sha256:6e36caf430ff6485c4b57a4c6b364a13f6a898d16b9417c6c37467e59c14b05a" + ], + "version": "==2024.1.8" + }, "typing-extensions": { "hashes": [ "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", diff --git a/lint-workflow-v2/README.md b/lint-workflow-v2/README.md index 4134ac42..173b43b6 100644 --- a/lint-workflow-v2/README.md +++ b/lint-workflow-v2/README.md @@ -1,15 +1,45 @@ -# lint-workflow +# Bitwarden Workflow Linter + +## Installation + +## PyPi +``` +Not yet implemented +``` + +### Locally +``` +git clone git@github.com:bitwarden/gh-actions.git +cd gh-actions/lint-workflow-v2 + +pip install -e . +``` ## Usage +### Setup settings.py + +After installation, copy the below and create a `settings.py` in the directory that `bwwl` will be running from. +```python +# settings.py.example +import json -There is currently NO packaging or distribution of this CLI tool. Until such time, the `cli.py` file needs to be run -with python 3.11+. -`python cli.py --help` +enabled_rules = [ + "src.rules.name_exists.RuleNameExists", + "src.rules.name_capitalized.RuleNameCapitalized", + "src.rules.pinned_job_runner.RuleJobRunnerVersionPinned", + "src.rules.job_environment_prefix.RuleJobEnvironmentPrefix", + "src.rules.step_pinned.RuleStepUsesPinned", +] + + +with open("actions.json", "r", encoding="utf8") as action_file: + approved_actions = json.load(action_file) +``` ``` -usage: workflow-linter [-h] [-v] {lint,actions} ... +usage: bwwl [-h] [-v] {lint,actions} ... positional arguments: {lint,actions} diff --git a/lint-workflow-v2/Taskfile.yml b/lint-workflow-v2/Taskfile.yml index c5b1afeb..dc82f835 100644 --- a/lint-workflow-v2/Taskfile.yml +++ b/lint-workflow-v2/Taskfile.yml @@ -18,6 +18,11 @@ tasks: cmds: - pipenv run pytype src + install: + silent: true + cmds: + - pipenv run python -m pip install -e . + test:unit: cmds: - pipenv run pytest tests @@ -49,3 +54,8 @@ tasks: test:e2e:actions:update: cmds: - pipenv run python cli.py -v actions update --output test.json + + dist: + silent: true + cmds: + - pipenv run python -m build diff --git a/lint-workflow-v2/actions.json b/lint-workflow-v2/actions.json.bak similarity index 100% rename from lint-workflow-v2/actions.json rename to lint-workflow-v2/actions.json.bak diff --git a/lint-workflow-v2/flake.nix b/lint-workflow-v2/flake.nix index f8f65776..3ed8e30a 100644 --- a/lint-workflow-v2/flake.nix +++ b/lint-workflow-v2/flake.nix @@ -2,8 +2,8 @@ description = "GitHub Action Linter"; inputs = { nixpkgs.url = "github:nixos/nixpkgs/22.11"; }; - outputs = { self, nixpkgs, flake-utils}: - flake-utils.lib.eachDefaultSystem (system: + outputs = { self, nixpkgs, flake-utils}: + flake-utils.lib.eachDefaultSystem (system: let #pkgs = nixpkgs.legacyPackages.x86_64-linux.pkgs; pkgs = nixpkgs.legacyPackages.${system}; diff --git a/lint-workflow-v2/pyproject.toml b/lint-workflow-v2/pyproject.toml new file mode 100644 index 00000000..2a6b9ac7 --- /dev/null +++ b/lint-workflow-v2/pyproject.toml @@ -0,0 +1,29 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "bitwarden-workflow-linter" +dynamic = ["version"] +authors = [] +description = "Custom GitHub Action Workflow Linter" +readme = "README.md" +requires-python = ">=3.11" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Operating System :: OS Independent", +] + +[project.urls] +Homepage = "https://github.com/bitwarden/gh-actions/tree/main/lint-workflow-v2" +Issues = "https://github.com/bitwarden/gh-actions/issues" + +[project.scripts] +bwwl = "src.cli:main" + +[tool.hatch.build.targets.wheel] +packages = ["src"] + +[tool.hatch.version] +path = "src/__about__.py" diff --git a/lint-workflow-v2/settings.py b/lint-workflow-v2/settings.py index f3fa66b2..f3f3f627 100644 --- a/lint-workflow-v2/settings.py +++ b/lint-workflow-v2/settings.py @@ -1,4 +1,5 @@ """This psuedo-module is essentially configuration-as-code.""" + import json @@ -11,5 +12,7 @@ ] -with open("actions.json", "r", encoding="utf8") as action_file: - approved_actions = json.load(action_file) +# with open("actions.json", "r", encoding="utf8") as action_file: +# approved_actions = json.load(action_file) + +# approved_actions_path = "" diff --git a/lint-workflow-v2/src/__about__.py b/lint-workflow-v2/src/__about__.py new file mode 100644 index 00000000..5820a729 --- /dev/null +++ b/lint-workflow-v2/src/__about__.py @@ -0,0 +1 @@ +version = "0.0.1" diff --git a/lint-workflow-v2/src/actions.py b/lint-workflow-v2/src/actions.py index 0ff30b4b..74d09ae4 100644 --- a/lint-workflow-v2/src/actions.py +++ b/lint-workflow-v2/src/actions.py @@ -1,4 +1,5 @@ """Module providing Actions subcommand to manage list of pre-approved Actions.""" + import argparse import json import logging diff --git a/lint-workflow-v2/cli.py b/lint-workflow-v2/src/cli.py similarity index 72% rename from lint-workflow-v2/cli.py rename to lint-workflow-v2/src/cli.py index 4da10380..a8c16840 100644 --- a/lint-workflow-v2/cli.py +++ b/lint-workflow-v2/src/cli.py @@ -1,24 +1,14 @@ """This is the entrypoint module for the workflow-linter CLI.""" + import argparse import sys -import settings -from src.actions import ActionsCmd -from src.utils import Settings, SettingsError -from src.lint import LinterCmd +from .actions import ActionsCmd +from .utils import Settings +from .lint import LinterCmd -try: - local_settings = Settings( - enabled_rules=settings.enabled_rules, approved_actions=settings.approved_actions - ) -except Exception as exc: - raise SettingsError( - ( - "Required settings: enabled_rules, approved_actions\n" - "Please see documentation for more information" - ) - ) from exc +local_settings = Settings.factory() def main(input_args: list[str] = None) -> int: @@ -32,7 +22,7 @@ def main(input_args: list[str] = None) -> int: actions_cmd = ActionsCmd(settings=local_settings) # Read arguments from command line. - parser = argparse.ArgumentParser(prog="workflow-linter") + parser = argparse.ArgumentParser(prog="bwwl") parser.add_argument("-v", "--verbose", action="store_true", default=False) subparsers = parser.add_subparsers(required=True, dest="command") diff --git a/lint-workflow-v2/src/default_actions.json b/lint-workflow-v2/src/default_actions.json new file mode 100644 index 00000000..f601cb89 --- /dev/null +++ b/lint-workflow-v2/src/default_actions.json @@ -0,0 +1,262 @@ +{ + "Asana/create-app-attachment-github-action": { + "name": "Asana/create-app-attachment-github-action", + "sha": "affc72d57bac733d864d4189ed69a9cbd61a9e4f", + "version": "v1.3" + }, + "Azure/functions-action": { + "name": "Azure/functions-action", + "sha": "238dc3c45bb1b04e5d16ff9e75cddd1d86753bd6", + "version": "v1.5.1" + }, + "Azure/get-keyvault-secrets": { + "name": "Azure/get-keyvault-secrets", + "sha": "b5c723b9ac7870c022b8c35befe620b7009b336f", + "version": "v1" + }, + "Azure/login": { + "name": "Azure/login", + "sha": "de95379fe4dadc2defb305917eaa7e5dde727294", + "version": "v1.5.1" + }, + "Swatinem/rust-cache": { + "name": "Swatinem/rust-cache", + "sha": "a95ba195448af2da9b00fb742d14ffaaf3c21f43", + "version": "v2.7.0" + }, + "SwiftDocOrg/github-wiki-publish-action": { + "name": "SwiftDocOrg/github-wiki-publish-action", + "sha": "a87db85ed06e4431be29cfdcb22b9653881305d0", + "version": "1.0.0" + }, + "SwiftDocOrg/swift-doc": { + "name": "SwiftDocOrg/swift-doc", + "sha": "f935ebfe524a0ff27bda07dadc3662e3e45b5125", + "version": "1.0.0-rc.1" + }, + "act10ns/slack": { + "name": "act10ns/slack", + "sha": "ed1309ab9862e57e9e583e51c7889486b9a00b0f", + "version": "v2.0.0" + }, + "actions/cache": { + "name": "actions/cache", + "sha": "704facf57e6136b1bc63b828d79edcd491f0ee84", + "version": "v3.3.2" + }, + "actions/checkout": { + "name": "actions/checkout", + "sha": "b4ffde65f46336ab88eb53be808477a3936bae11", + "version": "v4.1.1" + }, + "actions/delete-package-versions": { + "name": "actions/delete-package-versions", + "sha": "0d39a63126868f5eefaa47169615edd3c0f61e20", + "version": "v4.1.1" + }, + "actions/download-artifact": { + "name": "actions/download-artifact", + "sha": "f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110", + "version": "v4.1.0" + }, + "actions/github-script": { + "name": "actions/github-script", + "sha": "60a0d83039c74a4aee543508d2ffcb1c3799cdea", + "version": "v7.0.1" + }, + "actions/labeler": { + "name": "actions/labeler", + "sha": "8558fd74291d67161a8a78ce36a881fa63b766a9", + "version": "v5.0.0" + }, + "actions/setup-dotnet": { + "name": "actions/setup-dotnet", + "sha": "4d6c8fcf3c8f7a60068d26b594648e99df24cee3", + "version": "v4.0.0" + }, + "actions/setup-java": { + "name": "actions/setup-java", + "sha": "387ac29b308b003ca37ba93a6cab5eb57c8f5f93", + "version": "v4.0.0" + }, + "actions/setup-node": { + "name": "actions/setup-node", + "sha": "b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8", + "version": "v4.0.1" + }, + "actions/setup-python": { + "name": "actions/setup-python", + "sha": "0a5c61591373683505ea898e09a3ea4f39ef2b9c", + "version": "v5.0.0" + }, + "actions/stale": { + "name": "actions/stale", + "sha": "28ca1036281a5e5922ead5184a1bbf96e5fc984e", + "version": "v9.0.0" + }, + "actions/upload-artifact": { + "name": "actions/upload-artifact", + "sha": "c7d193f32edcb7bfad88892161225aeda64e9392", + "version": "v4.0.0" + }, + "android-actions/setup-android": { + "name": "android-actions/setup-android", + "sha": "07976c6290703d34c16d382cb36445f98bb43b1f", + "version": "v3.2.0" + }, + "azure/webapps-deploy": { + "name": "azure/webapps-deploy", + "sha": "145a0687697df1d8a28909569f6e5d86213041f9", + "version": "v3.0.0" + }, + "bitwarden/sm-action": { + "name": "bitwarden/sm-action", + "sha": "92d1d6a4f26a89a8191c83ab531a53544578f182", + "version": "v2.0.0" + }, + "checkmarx/ast-github-action": { + "name": "checkmarx/ast-github-action", + "sha": "72d549beebd0bc5bbafa559f198161b6ce7c03df", + "version": "2.0.21" + }, + "chrnorm/deployment-action": { + "name": "chrnorm/deployment-action", + "sha": "d42cde7132fcec920de534fffc3be83794335c00", + "version": "v2.0.5" + }, + "chrnorm/deployment-status": { + "name": "chrnorm/deployment-status", + "sha": "2afb7d27101260f4a764219439564d954d10b5b0", + "version": "v2.0.1" + }, + "chromaui/action": { + "name": "chromaui/action", + "sha": "80bf5911f28005ed208f15b7268843b79ca0e23a", + "version": "v1" + }, + "cloudflare/pages-action": { + "name": "cloudflare/pages-action", + "sha": "f0a1cd58cd66095dee69bfa18fa5efd1dde93bca", + "version": "v1.5.0" + }, + "convictional/trigger-workflow-and-wait": { + "name": "convictional/trigger-workflow-and-wait", + "sha": "f69fa9eedd3c62a599220f4d5745230e237904be", + "version": "v1.6.5" + }, + "crazy-max/ghaction-import-gpg": { + "name": "crazy-max/ghaction-import-gpg", + "sha": "01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4", + "version": "v6.1.0" + }, + "crowdin/github-action": { + "name": "crowdin/github-action", + "sha": "fdc55cdc519e86e32c22a07528d649277f1127f2", + "version": "v1.16.0" + }, + "dawidd6/action-download-artifact": { + "name": "dawidd6/action-download-artifact", + "sha": "e7466d1a7587ed14867642c2ca74b5bcc1e19a2d", + "version": "v3.0.0" + }, + "dawidd6/action-homebrew-bump-formula": { + "name": "dawidd6/action-homebrew-bump-formula", + "sha": "75ed025ff3ad1d617862838b342b06d613a0ddf3", + "version": "v3.10.1" + }, + "digitalocean/action-doctl": { + "name": "digitalocean/action-doctl", + "sha": "e5cb5b0cde9789f79c5115c2c4d902f38a708804", + "version": "v2.5.0" + }, + "docker/build-push-action": { + "name": "docker/build-push-action", + "sha": "4a13e500e55cf31b7a5d59a38ab2040ab0f42f56", + "version": "v5.1.0" + }, + "docker/setup-buildx-action": { + "name": "docker/setup-buildx-action", + "sha": "f95db51fddba0c2d1ec667646a06c2ce06100226", + "version": "v3.0.0" + }, + "docker/setup-qemu-action": { + "name": "docker/setup-qemu-action", + "sha": "68827325e0b33c7199eb31dd4e31fbe9023e06e3", + "version": "v3.0.0" + }, + "dorny/test-reporter": { + "name": "dorny/test-reporter", + "sha": "afe6793191b75b608954023a46831a3fe10048d4", + "version": "v1.7.0" + }, + "dtolnay/rust-toolchain": { + "name": "dtolnay/rust-toolchain", + "sha": "1482605bfc5719782e1267fd0c0cc350fe7646b8", + "version": "v1" + }, + "futureware-tech/simulator-action": { + "name": "futureware-tech/simulator-action", + "sha": "bfa03d93ec9de6dacb0c5553bbf8da8afc6c2ee9", + "version": "v3" + }, + "hashicorp/setup-packer": { + "name": "hashicorp/setup-packer", + "sha": "ecc5516821087666a672c0d280a0084ea6d9aafd", + "version": "v2.0.1" + }, + "macauley/action-homebrew-bump-cask": { + "name": "macauley/action-homebrew-bump-cask", + "sha": "445c42390d790569d938f9068d01af39ca030feb", + "version": "v1.0.0" + }, + "microsoft/setup-msbuild": { + "name": "microsoft/setup-msbuild", + "sha": "1ff57057b5cfdc39105cd07a01d78e9b0ea0c14c", + "version": "v1.3.1" + }, + "ncipollo/release-action": { + "name": "ncipollo/release-action", + "sha": "6c75be85e571768fa31b40abf38de58ba0397db5", + "version": "v1.13.0" + }, + "peter-evans/close-issue": { + "name": "peter-evans/close-issue", + "sha": "276d7966e389d888f011539a86c8920025ea0626", + "version": "v3.0.1" + }, + "ruby/setup-ruby": { + "name": "ruby/setup-ruby", + "sha": "360dc864d5da99d54fcb8e9148c14a84b90d3e88", + "version": "v1.165.1" + }, + "samuelmeuli/action-snapcraft": { + "name": "samuelmeuli/action-snapcraft", + "sha": "d33c176a9b784876d966f80fb1b461808edc0641", + "version": "v2.1.1" + }, + "snapcore/action-build": { + "name": "snapcore/action-build", + "sha": "2096990827aa966f773676c8a53793c723b6b40f", + "version": "v1.2.0" + }, + "sonarsource/sonarcloud-github-action": { + "name": "sonarsource/sonarcloud-github-action", + "sha": "49e6cd3b187936a73b8280d59ffd9da69df63ec9", + "version": "v2.1.1" + }, + "stackrox/kube-linter-action": { + "name": "stackrox/kube-linter-action", + "sha": "ca0d55b925470deb5b04b556e6c4276ea94d03c3", + "version": "v1.0.4" + }, + "tj-actions/changed-files": { + "name": "tj-actions/changed-files", + "sha": "716b1e13042866565e00e85fd4ec490e186c4a2f", + "version": "v41.0.1" + }, + "yogevbd/enforce-label-action": { + "name": "yogevbd/enforce-label-action", + "sha": "a3c219da6b8fa73f6ba62b68ff09c469b3a1c024", + "version": "2.2.2" + } +} diff --git a/lint-workflow-v2/src/default_settings.py b/lint-workflow-v2/src/default_settings.py new file mode 100644 index 00000000..272c3316 --- /dev/null +++ b/lint-workflow-v2/src/default_settings.py @@ -0,0 +1,20 @@ +"""This psuedo-module is essentially configuration-as-code.""" + +import json + + +enabled_rules = [ + "src.rules.name_exists.RuleNameExists", + "src.rules.name_capitalized.RuleNameCapitalized", + "src.rules.pinned_job_runner.RuleJobRunnerVersionPinned", + "src.rules.job_environment_prefix.RuleJobEnvironmentPrefix", + "src.rules.step_pinned.RuleStepUsesPinned", +] + + +# with open("actions.json", "r", encoding="utf8") as action_file: +# approved_actions = json.load(action_file) + +# approved_actions = [] + +approved_actions_path = "default_actions.json" diff --git a/lint-workflow-v2/src/lint.py b/lint-workflow-v2/src/lint.py index 95025e73..8cf24344 100644 --- a/lint-workflow-v2/src/lint.py +++ b/lint-workflow-v2/src/lint.py @@ -1,5 +1,6 @@ """Module providing Lint subcommand to run custom linting rules against GitHub Action Workflows.""" + import argparse import os diff --git a/lint-workflow-v2/src/load.py b/lint-workflow-v2/src/load.py index 8ffa6c51..6fbcf344 100644 --- a/lint-workflow-v2/src/load.py +++ b/lint-workflow-v2/src/load.py @@ -1,4 +1,5 @@ """Module to load for Worflows and Rules.""" + import importlib from typing import List, Optional diff --git a/lint-workflow-v2/src/models/job.py b/lint-workflow-v2/src/models/job.py index f01913c0..c880bfc0 100644 --- a/lint-workflow-v2/src/models/job.py +++ b/lint-workflow-v2/src/models/job.py @@ -1,4 +1,5 @@ """Representation for a job in a GitHub Action workflow.""" + from dataclasses import dataclass, field from typing import List, Optional, Self diff --git a/lint-workflow-v2/src/models/step.py b/lint-workflow-v2/src/models/step.py index a97ce917..34012fa0 100644 --- a/lint-workflow-v2/src/models/step.py +++ b/lint-workflow-v2/src/models/step.py @@ -1,4 +1,5 @@ """Representation for a job step in a GitHub Action workflow.""" + from dataclasses import dataclass, field from typing import Optional, Self diff --git a/lint-workflow-v2/src/models/workflow.py b/lint-workflow-v2/src/models/workflow.py index aef36de8..45733e68 100644 --- a/lint-workflow-v2/src/models/workflow.py +++ b/lint-workflow-v2/src/models/workflow.py @@ -1,4 +1,5 @@ """Representation for an entire GitHub Action workflow.""" + from dataclasses import dataclass from typing import Dict, Optional diff --git a/lint-workflow-v2/src/rule.py b/lint-workflow-v2/src/rule.py index 59a79f7a..3149cf69 100644 --- a/lint-workflow-v2/src/rule.py +++ b/lint-workflow-v2/src/rule.py @@ -1,4 +1,5 @@ """Base Rule class to build rules by extending.""" + from typing import List, Optional, Tuple, Union from .models.workflow import Workflow diff --git a/lint-workflow-v2/src/rules/job_environment_prefix.py b/lint-workflow-v2/src/rules/job_environment_prefix.py index ed801091..94040871 100644 --- a/lint-workflow-v2/src/rules/job_environment_prefix.py +++ b/lint-workflow-v2/src/rules/job_environment_prefix.py @@ -1,4 +1,5 @@ """A Rule to enforce prefixes environment variables.""" + from typing import Union, Optional, Tuple, List from ..models.job import Job diff --git a/lint-workflow-v2/src/rules/name_capitalized.py b/lint-workflow-v2/src/rules/name_capitalized.py index 340afa91..71cb2598 100644 --- a/lint-workflow-v2/src/rules/name_capitalized.py +++ b/lint-workflow-v2/src/rules/name_capitalized.py @@ -1,4 +1,5 @@ """A Rule to enforce all 'name' values start with a capital letter.""" + from typing import Optional, Tuple, Union from ..models.workflow import Workflow diff --git a/lint-workflow-v2/src/rules/name_exists.py b/lint-workflow-v2/src/rules/name_exists.py index 73f204d8..124214a9 100644 --- a/lint-workflow-v2/src/rules/name_exists.py +++ b/lint-workflow-v2/src/rules/name_exists.py @@ -1,4 +1,5 @@ """A Rule to enforce that a 'name' exists.""" + from typing import Optional, Tuple, Union from ..models.workflow import Workflow diff --git a/lint-workflow-v2/src/rules/pinned_job_runner.py b/lint-workflow-v2/src/rules/pinned_job_runner.py index 6397c3ea..2124c64a 100644 --- a/lint-workflow-v2/src/rules/pinned_job_runner.py +++ b/lint-workflow-v2/src/rules/pinned_job_runner.py @@ -1,4 +1,5 @@ """A Rule to enforce pinning runners to a specific OS version.""" + from typing import List, Optional, Tuple, Union from ..models.job import Job diff --git a/lint-workflow-v2/src/rules/step_approved.py b/lint-workflow-v2/src/rules/step_approved.py index 06ab20b1..0b820438 100644 --- a/lint-workflow-v2/src/rules/step_approved.py +++ b/lint-workflow-v2/src/rules/step_approved.py @@ -1,4 +1,5 @@ """A Rule to enforce the use of a list of pre-approved Actions.""" + from typing import List, Optional, Tuple, Union from ..models.job import Job diff --git a/lint-workflow-v2/src/rules/step_pinned.py b/lint-workflow-v2/src/rules/step_pinned.py index 8aa6c3ca..c9b2bcac 100644 --- a/lint-workflow-v2/src/rules/step_pinned.py +++ b/lint-workflow-v2/src/rules/step_pinned.py @@ -1,4 +1,5 @@ """A Rule to enforce Actions are pinned correctly.""" + from typing import List, Optional, Tuple, Union from ..models.job import Job diff --git a/lint-workflow-v2/src/utils.py b/lint-workflow-v2/src/utils.py index e7a6395a..0b15b47d 100644 --- a/lint-workflow-v2/src/utils.py +++ b/lint-workflow-v2/src/utils.py @@ -1,8 +1,16 @@ """Module of a collection of random utilities.""" + +import importlib.resources +import json +import os + from dataclasses import dataclass from enum import Enum from typing import Optional, Self +from .default_settings import enabled_rules as default_enabled_rules +from .default_settings import approved_actions_path as default_approved_actions_path + @dataclass class Colors: @@ -127,3 +135,30 @@ def __init__( self.approved_actions = { name: Action(**action) for name, action in approved_actions.items() } + + @staticmethod + def factory() -> Self: + enabled_rules = default_enabled_rules + approved_actions_file = default_approved_actions_path + + try: + if os.path.exists("settings.py"): + import settings + + if hasattr(settings, "enabled_rules"): + enabled_rules = settings.enabled_rules + + if hasattr(settings, "approved_actions_path"): + if os.path.exists(settings.approved_actions_path): + with open( + settings.approved_actions_path, "r", encoding="utf8" + ) as action_file: + approved_actions = json.load(action_file) + + else: + approved_actions = {} + + except Exception as exc: + raise SettingsError("Error importing settings") from exc + + return Settings(enabled_rules=enabled_rules, approved_actions=approved_actions) diff --git a/lint-workflow-v2/tests/rules/test_job_environment_prefix.py b/lint-workflow-v2/tests/rules/test_job_environment_prefix.py index 832d9713..449c87c5 100644 --- a/lint-workflow-v2/tests/rules/test_job_environment_prefix.py +++ b/lint-workflow-v2/tests/rules/test_job_environment_prefix.py @@ -1,4 +1,5 @@ """Test src/rules/job_environment_prefix.""" + import pytest from ruamel.yaml import YAML diff --git a/lint-workflow-v2/tests/rules/test_name_capitalized.py b/lint-workflow-v2/tests/rules/test_name_capitalized.py index 41120175..45d01498 100644 --- a/lint-workflow-v2/tests/rules/test_name_capitalized.py +++ b/lint-workflow-v2/tests/rules/test_name_capitalized.py @@ -1,4 +1,5 @@ """Test src/rules/name_capitalized.py.""" + import pytest from ruamel.yaml import YAML diff --git a/lint-workflow-v2/tests/rules/test_name_exists.py b/lint-workflow-v2/tests/rules/test_name_exists.py index 2d2ed977..4ca074a2 100644 --- a/lint-workflow-v2/tests/rules/test_name_exists.py +++ b/lint-workflow-v2/tests/rules/test_name_exists.py @@ -1,4 +1,5 @@ """Test src/rules/name_exists.py.""" + import pytest from ruamel.yaml import YAML diff --git a/lint-workflow-v2/tests/rules/test_pinned_job_runner.py b/lint-workflow-v2/tests/rules/test_pinned_job_runner.py index e3facfad..0b24c722 100644 --- a/lint-workflow-v2/tests/rules/test_pinned_job_runner.py +++ b/lint-workflow-v2/tests/rules/test_pinned_job_runner.py @@ -1,4 +1,5 @@ """Test src/rules/pinned_job_runner.py.""" + import pytest from ruamel.yaml import YAML diff --git a/lint-workflow-v2/tests/rules/test_step_approved.py b/lint-workflow-v2/tests/rules/test_step_approved.py index c343fecd..6db227ba 100644 --- a/lint-workflow-v2/tests/rules/test_step_approved.py +++ b/lint-workflow-v2/tests/rules/test_step_approved.py @@ -1,4 +1,5 @@ """Test src/rules/step_approved.py.""" + import pytest from ruamel.yaml import YAML diff --git a/lint-workflow-v2/tests/rules/test_step_pinned.py b/lint-workflow-v2/tests/rules/test_step_pinned.py index 101d7922..c4ad5c41 100644 --- a/lint-workflow-v2/tests/rules/test_step_pinned.py +++ b/lint-workflow-v2/tests/rules/test_step_pinned.py @@ -1,4 +1,5 @@ """Test src/rules/step_pinned.py.""" + import pytest from ruamel.yaml import YAML diff --git a/lint-workflow-v2/tests/test_job.py b/lint-workflow-v2/tests/test_job.py index eb6f879b..32481cd8 100644 --- a/lint-workflow-v2/tests/test_job.py +++ b/lint-workflow-v2/tests/test_job.py @@ -1,4 +1,5 @@ """Test src/models/job.py.""" + import pytest from src.models.job import Job diff --git a/lint-workflow-v2/tests/test_lint.py b/lint-workflow-v2/tests/test_lint.py index ce4f3ec0..3b19f806 100644 --- a/lint-workflow-v2/tests/test_lint.py +++ b/lint-workflow-v2/tests/test_lint.py @@ -1,4 +1,5 @@ """Test src/lint.py.""" + import pytest from src.lint import LinterCmd diff --git a/lint-workflow-v2/tests/test_load.py b/lint-workflow-v2/tests/test_load.py index 1ec34222..02ee473e 100644 --- a/lint-workflow-v2/tests/test_load.py +++ b/lint-workflow-v2/tests/test_load.py @@ -1,4 +1,5 @@ """Tests src/load.py.""" + import pytest from ruamel.yaml import YAML diff --git a/lint-workflow-v2/tests/test_rule.py b/lint-workflow-v2/tests/test_rule.py index 9eb348e2..10a1470e 100644 --- a/lint-workflow-v2/tests/test_rule.py +++ b/lint-workflow-v2/tests/test_rule.py @@ -1,4 +1,5 @@ """Tests src/rule.py.""" + import pytest from typing import Union diff --git a/lint-workflow-v2/tests/test_step.py b/lint-workflow-v2/tests/test_step.py index 9acd2e81..da2ec383 100644 --- a/lint-workflow-v2/tests/test_step.py +++ b/lint-workflow-v2/tests/test_step.py @@ -1,4 +1,5 @@ """Test src/models/step.py.""" + import json import pytest diff --git a/lint-workflow-v2/tests/test_utils.py b/lint-workflow-v2/tests/test_utils.py index d79e12ac..3ca5333a 100644 --- a/lint-workflow-v2/tests/test_utils.py +++ b/lint-workflow-v2/tests/test_utils.py @@ -1,4 +1,5 @@ """Tests src/utils.py.""" + from src.utils import Action, Colors, LintFinding, LintLevels diff --git a/lint-workflow-v2/tests/test_workflow.py b/lint-workflow-v2/tests/test_workflow.py index 3e765fee..01b04ffb 100644 --- a/lint-workflow-v2/tests/test_workflow.py +++ b/lint-workflow-v2/tests/test_workflow.py @@ -1,4 +1,5 @@ """Test src/models/workflow.py.""" + import pytest from src.models.job import Job From b8c0b8a89351ad51c61ecbfece8b7cb2525fc319 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 2 Feb 2024 10:59:03 -0800 Subject: [PATCH 42/57] Update to Nix Unstable packages to allow for editable paths --- lint-workflow-v2/Pipfile | 1 + lint-workflow-v2/Pipfile.lock | 404 +++++++++++++++----------------- lint-workflow-v2/flake.lock | 14 +- lint-workflow-v2/flake.nix | 2 +- lint-workflow-v2/pyproject.toml | 11 +- 5 files changed, 210 insertions(+), 222 deletions(-) diff --git a/lint-workflow-v2/Pipfile b/lint-workflow-v2/Pipfile index 8dc6f95b..0067d699 100644 --- a/lint-workflow-v2/Pipfile +++ b/lint-workflow-v2/Pipfile @@ -9,6 +9,7 @@ urllib3 = "*" pydantic = "*" "ruamel.yaml" = "*" dataclasses-json = "*" +bitwarden-workflow-linter = {editable = true, path = "."} [dev-packages] black = "*" diff --git a/lint-workflow-v2/Pipfile.lock b/lint-workflow-v2/Pipfile.lock index 99f0822d..c13b2625 100644 --- a/lint-workflow-v2/Pipfile.lock +++ b/lint-workflow-v2/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "7e51933f1987a5e1d1182f4cec2a1c6f84194a17ac6653bb3674da0fe26dfb7f" + "sha256": "bff95bd7882b10ad96449f9b3bdb3e9cfae609496730fa578d9de49ec0c61735" }, "pipfile-spec": 6, "requires": { @@ -24,13 +24,17 @@ "markers": "python_version >= '3.8'", "version": "==0.6.0" }, + "bitwarden-workflow-linter": { + "editable": true, + "path": "." + }, "dataclasses-json": { "hashes": [ - "sha256:35cb40aae824736fdf959801356641836365219cfe14caeb115c39136f775d2a", - "sha256:4aeb343357997396f6bca1acae64e486c3a723d8f5c76301888abeccf0c45176" + "sha256:73696ebf24936560cca79a2430cbc4f3dd23ac7bf46ed17f38e5e5e7657a6377", + "sha256:f90578b8a3177f7552f4e1a6e535e84293cd5da421fcce0642d49c0d7bdf8df2" ], "index": "pypi", - "version": "==0.6.3" + "version": "==0.6.4" }, "marshmallow": { "hashes": [ @@ -58,122 +62,96 @@ }, "pydantic": { "hashes": [ - "sha256:b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a", - "sha256:d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4" + "sha256:1440966574e1b5b99cf75a13bec7b20e3512e8a61b894ae252f56275e2c465ae", + "sha256:ae887bd94eb404b09d86e4d12f93893bdca79d766e738528c6fa1c849f3c6bcf" ], "index": "pypi", - "version": "==2.5.3" + "version": "==2.6.0" }, "pydantic-core": { "hashes": [ - "sha256:00646784f6cd993b1e1c0e7b0fdcbccc375d539db95555477771c27555e3c556", - "sha256:00b1087dabcee0b0ffd104f9f53d7d3eaddfaa314cdd6726143af6bc713aa27e", - "sha256:0348b1dc6b76041516e8a854ff95b21c55f5a411c3297d2ca52f5528e49d8411", - "sha256:036137b5ad0cb0004c75b579445a1efccd072387a36c7f217bb8efd1afbe5245", - "sha256:095b707bb287bfd534044166ab767bec70a9bba3175dcdc3371782175c14e43c", - "sha256:0c08de15d50fa190d577e8591f0329a643eeaed696d7771760295998aca6bc66", - "sha256:1302a54f87b5cd8528e4d6d1bf2133b6aa7c6122ff8e9dc5220fbc1e07bffebd", - "sha256:172de779e2a153d36ee690dbc49c6db568d7b33b18dc56b69a7514aecbcf380d", - "sha256:1b027c86c66b8627eb90e57aee1f526df77dc6d8b354ec498be9a757d513b92b", - "sha256:1ce830e480f6774608dedfd4a90c42aac4a7af0a711f1b52f807130c2e434c06", - "sha256:1fd0c1d395372843fba13a51c28e3bb9d59bd7aebfeb17358ffaaa1e4dbbe948", - "sha256:23598acb8ccaa3d1d875ef3b35cb6376535095e9405d91a3d57a8c7db5d29341", - "sha256:24368e31be2c88bd69340fbfe741b405302993242ccb476c5c3ff48aeee1afe0", - "sha256:26a92ae76f75d1915806b77cf459811e772d8f71fd1e4339c99750f0e7f6324f", - "sha256:27e524624eace5c59af499cd97dc18bb201dc6a7a2da24bfc66ef151c69a5f2a", - "sha256:2b8719037e570639e6b665a4050add43134d80b687288ba3ade18b22bbb29dd2", - "sha256:2c5bcf3414367e29f83fd66f7de64509a8fd2368b1edf4351e862910727d3e51", - "sha256:2dbe357bc4ddda078f79d2a36fc1dd0494a7f2fad83a0a684465b6f24b46fe80", - "sha256:2f5fa187bde8524b1e37ba894db13aadd64faa884657473b03a019f625cee9a8", - "sha256:2f6ffc6701a0eb28648c845f4945a194dc7ab3c651f535b81793251e1185ac3d", - "sha256:314ccc4264ce7d854941231cf71b592e30d8d368a71e50197c905874feacc8a8", - "sha256:36026d8f99c58d7044413e1b819a67ca0e0b8ebe0f25e775e6c3d1fabb3c38fb", - "sha256:36099c69f6b14fc2c49d7996cbf4f87ec4f0e66d1c74aa05228583225a07b590", - "sha256:36fa402dcdc8ea7f1b0ddcf0df4254cc6b2e08f8cd80e7010d4c4ae6e86b2a87", - "sha256:370ffecb5316ed23b667d99ce4debe53ea664b99cc37bfa2af47bc769056d534", - "sha256:3860c62057acd95cc84044e758e47b18dcd8871a328ebc8ccdefd18b0d26a21b", - "sha256:399ac0891c284fa8eb998bcfa323f2234858f5d2efca3950ae58c8f88830f145", - "sha256:3a0b5db001b98e1c649dd55afa928e75aa4087e587b9524a4992316fa23c9fba", - "sha256:3dcf1978be02153c6a31692d4fbcc2a3f1db9da36039ead23173bc256ee3b91b", - "sha256:4241204e4b36ab5ae466ecec5c4c16527a054c69f99bba20f6f75232a6a534e2", - "sha256:438027a975cc213a47c5d70672e0d29776082155cfae540c4e225716586be75e", - "sha256:43e166ad47ba900f2542a80d83f9fc65fe99eb63ceec4debec160ae729824052", - "sha256:478e9e7b360dfec451daafe286998d4a1eeaecf6d69c427b834ae771cad4b622", - "sha256:4ce8299b481bcb68e5c82002b96e411796b844d72b3e92a3fbedfe8e19813eab", - "sha256:4f86f1f318e56f5cbb282fe61eb84767aee743ebe32c7c0834690ebea50c0a6b", - "sha256:55a23dcd98c858c0db44fc5c04fc7ed81c4b4d33c653a7c45ddaebf6563a2f66", - "sha256:599c87d79cab2a6a2a9df4aefe0455e61e7d2aeede2f8577c1b7c0aec643ee8e", - "sha256:5aa90562bc079c6c290f0512b21768967f9968e4cfea84ea4ff5af5d917016e4", - "sha256:64634ccf9d671c6be242a664a33c4acf12882670b09b3f163cd00a24cffbd74e", - "sha256:667aa2eac9cd0700af1ddb38b7b1ef246d8cf94c85637cbb03d7757ca4c3fdec", - "sha256:6a31d98c0d69776c2576dda4b77b8e0c69ad08e8b539c25c7d0ca0dc19a50d6c", - "sha256:6af4b3f52cc65f8a0bc8b1cd9676f8c21ef3e9132f21fed250f6958bd7223bed", - "sha256:6c8edaea3089bf908dd27da8f5d9e395c5b4dc092dbcce9b65e7156099b4b937", - "sha256:71d72ca5eaaa8d38c8df16b7deb1a2da4f650c41b58bb142f3fb75d5ad4a611f", - "sha256:72f9a942d739f09cd42fffe5dc759928217649f070056f03c70df14f5770acf9", - "sha256:747265448cb57a9f37572a488a57d873fd96bf51e5bb7edb52cfb37124516da4", - "sha256:75ec284328b60a4e91010c1acade0c30584f28a1f345bc8f72fe8b9e46ec6a96", - "sha256:78d0768ee59baa3de0f4adac9e3748b4b1fffc52143caebddfd5ea2961595277", - "sha256:78ee52ecc088c61cce32b2d30a826f929e1708f7b9247dc3b921aec367dc1b23", - "sha256:7be719e4d2ae6c314f72844ba9d69e38dff342bc360379f7c8537c48e23034b7", - "sha256:7e1f4744eea1501404b20b0ac059ff7e3f96a97d3e3f48ce27a139e053bb370b", - "sha256:7e90d6cc4aad2cc1f5e16ed56e46cebf4877c62403a311af20459c15da76fd91", - "sha256:7ebe3416785f65c28f4f9441e916bfc8a54179c8dea73c23023f7086fa601c5d", - "sha256:7f41533d7e3cf9520065f610b41ac1c76bc2161415955fbcead4981b22c7611e", - "sha256:7f5025db12fc6de7bc1104d826d5aee1d172f9ba6ca936bf6474c2148ac336c1", - "sha256:86c963186ca5e50d5c8287b1d1c9d3f8f024cbe343d048c5bd282aec2d8641f2", - "sha256:86ce5fcfc3accf3a07a729779d0b86c5d0309a4764c897d86c11089be61da160", - "sha256:8a14c192c1d724c3acbfb3f10a958c55a2638391319ce8078cb36c02283959b9", - "sha256:8b93785eadaef932e4fe9c6e12ba67beb1b3f1e5495631419c784ab87e975670", - "sha256:8ed1af8692bd8d2a29d702f1a2e6065416d76897d726e45a1775b1444f5928a7", - "sha256:92879bce89f91f4b2416eba4429c7b5ca22c45ef4a499c39f0c5c69257522c7c", - "sha256:94fc0e6621e07d1e91c44e016cc0b189b48db053061cc22d6298a611de8071bb", - "sha256:982487f8931067a32e72d40ab6b47b1628a9c5d344be7f1a4e668fb462d2da42", - "sha256:9862bf828112e19685b76ca499b379338fd4c5c269d897e218b2ae8fcb80139d", - "sha256:99b14dbea2fdb563d8b5a57c9badfcd72083f6006caf8e126b491519c7d64ca8", - "sha256:9c6a5c79b28003543db3ba67d1df336f253a87d3112dac3a51b94f7d48e4c0e1", - "sha256:a19b794f8fe6569472ff77602437ec4430f9b2b9ec7a1105cfd2232f9ba355e6", - "sha256:a306cdd2ad3a7d795d8e617a58c3a2ed0f76c8496fb7621b6cd514eb1532cae8", - "sha256:a3dde6cac75e0b0902778978d3b1646ca9f438654395a362cb21d9ad34b24acf", - "sha256:a874f21f87c485310944b2b2734cd6d318765bcbb7515eead33af9641816506e", - "sha256:a983cca5ed1dd9a35e9e42ebf9f278d344603bfcb174ff99a5815f953925140a", - "sha256:aca48506a9c20f68ee61c87f2008f81f8ee99f8d7f0104bff3c47e2d148f89d9", - "sha256:b2602177668f89b38b9f84b7b3435d0a72511ddef45dc14446811759b82235a1", - "sha256:b3e5fe4538001bb82e2295b8d2a39356a84694c97cb73a566dc36328b9f83b40", - "sha256:b6ca36c12a5120bad343eef193cc0122928c5c7466121da7c20f41160ba00ba2", - "sha256:b89f4477d915ea43b4ceea6756f63f0288941b6443a2b28c69004fe07fde0d0d", - "sha256:b9a9d92f10772d2a181b5ca339dee066ab7d1c9a34ae2421b2a52556e719756f", - "sha256:c99462ffc538717b3e60151dfaf91125f637e801f5ab008f81c402f1dff0cd0f", - "sha256:cb92f9061657287eded380d7dc455bbf115430b3aa4741bdc662d02977e7d0af", - "sha256:cdee837710ef6b56ebd20245b83799fce40b265b3b406e51e8ccc5b85b9099b7", - "sha256:cf10b7d58ae4a1f07fccbf4a0a956d705356fea05fb4c70608bb6fa81d103cda", - "sha256:d15687d7d7f40333bd8266f3814c591c2e2cd263fa2116e314f60d82086e353a", - "sha256:d5c28525c19f5bb1e09511669bb57353d22b94cf8b65f3a8d141c389a55dec95", - "sha256:d5f916acf8afbcab6bacbb376ba7dc61f845367901ecd5e328fc4d4aef2fcab0", - "sha256:dab03ed811ed1c71d700ed08bde8431cf429bbe59e423394f0f4055f1ca0ea60", - "sha256:db453f2da3f59a348f514cfbfeb042393b68720787bbef2b4c6068ea362c8149", - "sha256:de2a0645a923ba57c5527497daf8ec5df69c6eadf869e9cd46e86349146e5975", - "sha256:dea7fcd62915fb150cdc373212141a30037e11b761fbced340e9db3379b892d4", - "sha256:dfcbebdb3c4b6f739a91769aea5ed615023f3c88cb70df812849aef634c25fbe", - "sha256:dfcebb950aa7e667ec226a442722134539e77c575f6cfaa423f24371bb8d2e94", - "sha256:e0641b506486f0b4cd1500a2a65740243e8670a2549bb02bc4556a83af84ae03", - "sha256:e33b0834f1cf779aa839975f9d8755a7c2420510c0fa1e9fa0497de77cd35d2c", - "sha256:e4ace1e220b078c8e48e82c081e35002038657e4b37d403ce940fa679e57113b", - "sha256:e4cf2d5829f6963a5483ec01578ee76d329eb5caf330ecd05b3edd697e7d768a", - "sha256:e574de99d735b3fc8364cba9912c2bec2da78775eba95cbb225ef7dda6acea24", - "sha256:e646c0e282e960345314f42f2cea5e0b5f56938c093541ea6dbf11aec2862391", - "sha256:e8a5ac97ea521d7bde7621d86c30e86b798cdecd985723c4ed737a2aa9e77d0c", - "sha256:eedf97be7bc3dbc8addcef4142f4b4164066df0c6f36397ae4aaed3eb187d8ab", - "sha256:ef633add81832f4b56d3b4c9408b43d530dfca29e68fb1b797dcb861a2c734cd", - "sha256:f27207e8ca3e5e021e2402ba942e5b4c629718e665c81b8b306f3c8b1ddbb786", - "sha256:f85f3843bdb1fe80e8c206fe6eed7a1caeae897e496542cee499c374a85c6e08", - "sha256:f8e81e4b55930e5ffab4a68db1af431629cf2e4066dbdbfef65348b8ab804ea8", - "sha256:f96ae96a060a8072ceff4cfde89d261837b4294a4f28b84a28765470d502ccc6", - "sha256:fd9e98b408384989ea4ab60206b8e100d8687da18b5c813c11e92fd8212a98e0", - "sha256:ffff855100bc066ff2cd3aa4a60bc9534661816b110f0243e59503ec2df38421" + "sha256:06f0d5a1d9e1b7932477c172cc720b3b23c18762ed7a8efa8398298a59d177c7", + "sha256:07982b82d121ed3fc1c51faf6e8f57ff09b1325d2efccaa257dd8c0dd937acca", + "sha256:0f478ec204772a5c8218e30eb813ca43e34005dff2eafa03931b3d8caef87d51", + "sha256:102569d371fadc40d8f8598a59379c37ec60164315884467052830b28cc4e9da", + "sha256:10dca874e35bb60ce4f9f6665bfbfad050dd7573596608aeb9e098621ac331dc", + "sha256:150ba5c86f502c040b822777e2e519b5625b47813bd05f9273a8ed169c97d9ae", + "sha256:1661c668c1bb67b7cec96914329d9ab66755911d093bb9063c4c8914188af6d4", + "sha256:1a2fe7b00a49b51047334d84aafd7e39f80b7675cad0083678c58983662da89b", + "sha256:1ae8048cba95f382dba56766525abca438328455e35c283bb202964f41a780b0", + "sha256:20f724a023042588d0f4396bbbcf4cffd0ddd0ad3ed4f0d8e6d4ac4264bae81e", + "sha256:2133b0e412a47868a358713287ff9f9a328879da547dc88be67481cdac529118", + "sha256:21e3298486c4ea4e4d5cc6fb69e06fb02a4e22089304308817035ac006a7f506", + "sha256:21ebaa4bf6386a3b22eec518da7d679c8363fb7fb70cf6972161e5542f470798", + "sha256:23632132f1fd608034f1a56cc3e484be00854db845b3a4a508834be5a6435a6f", + "sha256:2d5bea8012df5bb6dda1e67d0563ac50b7f64a5d5858348b5c8cb5043811c19d", + "sha256:300616102fb71241ff477a2cbbc847321dbec49428434a2f17f37528721c4948", + "sha256:30a8259569fbeec49cfac7fda3ec8123486ef1b729225222f0d41d5f840b476f", + "sha256:399166f24c33a0c5759ecc4801f040dbc87d412c1a6d6292b2349b4c505effc9", + "sha256:3fac641bbfa43d5a1bed99d28aa1fded1984d31c670a95aac1bf1d36ac6ce137", + "sha256:42c29d54ed4501a30cd71015bf982fa95e4a60117b44e1a200290ce687d3e640", + "sha256:462d599299c5971f03c676e2b63aa80fec5ebc572d89ce766cd11ca8bcb56f3f", + "sha256:4eebbd049008eb800f519578e944b8dc8e0f7d59a5abb5924cc2d4ed3a1834ff", + "sha256:502c062a18d84452858f8aea1e520e12a4d5228fc3621ea5061409d666ea1706", + "sha256:5317c04349472e683803da262c781c42c5628a9be73f4750ac7d13040efb5d2d", + "sha256:5511f962dd1b9b553e9534c3b9c6a4b0c9ded3d8c2be96e61d56f933feef9e1f", + "sha256:561be4e3e952c2f9056fba5267b99be4ec2afadc27261505d4992c50b33c513c", + "sha256:601d3e42452cd4f2891c13fa8c70366d71851c1593ed42f57bf37f40f7dca3c8", + "sha256:644904600c15816a1f9a1bafa6aab0d21db2788abcdf4e2a77951280473f33e1", + "sha256:653a5dfd00f601a0ed6654a8b877b18d65ac32c9d9997456e0ab240807be6cf7", + "sha256:694a5e9f1f2c124a17ff2d0be613fd53ba0c26de588eb4bdab8bca855e550d95", + "sha256:71b4a48a7427f14679f0015b13c712863d28bb1ab700bd11776a5368135c7d60", + "sha256:72bf9308a82b75039b8c8edd2be2924c352eda5da14a920551a8b65d5ee89253", + "sha256:735dceec50fa907a3c314b84ed609dec54b76a814aa14eb90da31d1d36873a5e", + "sha256:73802194f10c394c2bedce7a135ba1d8ba6cff23adf4217612bfc5cf060de34c", + "sha256:780daad9e35b18d10d7219d24bfb30148ca2afc309928e1d4d53de86822593dc", + "sha256:8655f55fe68c4685673265a650ef71beb2d31871c049c8b80262026f23605ee3", + "sha256:877045a7969ace04d59516d5d6a7dee13106822f99a5d8df5e6822941f7bedc8", + "sha256:87bce04f09f0552b66fca0c4e10da78d17cb0e71c205864bab4e9595122cb9d9", + "sha256:8d4dfc66abea3ec6d9f83e837a8f8a7d9d3a76d25c9911735c76d6745950e62c", + "sha256:8ec364e280db4235389b5e1e6ee924723c693cbc98e9d28dc1767041ff9bc388", + "sha256:8fa00fa24ffd8c31fac081bf7be7eb495be6d248db127f8776575a746fa55c95", + "sha256:920c4897e55e2881db6a6da151198e5001552c3777cd42b8a4c2f72eedc2ee91", + "sha256:920f4633bee43d7a2818e1a1a788906df5a17b7ab6fe411220ed92b42940f818", + "sha256:9795f56aa6b2296f05ac79d8a424e94056730c0b860a62b0fdcfe6340b658cc8", + "sha256:98f0edee7ee9cc7f9221af2e1b95bd02810e1c7a6d115cfd82698803d385b28f", + "sha256:99c095457eea8550c9fa9a7a992e842aeae1429dab6b6b378710f62bfb70b394", + "sha256:99d3a433ef5dc3021c9534a58a3686c88363c591974c16c54a01af7efd741f13", + "sha256:99f9a50b56713a598d33bc23a9912224fc5d7f9f292444e6664236ae471ddf17", + "sha256:9c46e556ee266ed3fb7b7a882b53df3c76b45e872fdab8d9cf49ae5e91147fd7", + "sha256:9f5d37ff01edcbace53a402e80793640c25798fb7208f105d87a25e6fcc9ea06", + "sha256:a0b4cfe408cd84c53bab7d83e4209458de676a6ec5e9c623ae914ce1cb79b96f", + "sha256:a497be217818c318d93f07e14502ef93d44e6a20c72b04c530611e45e54c2196", + "sha256:ac89ccc39cd1d556cc72d6752f252dc869dde41c7c936e86beac5eb555041b66", + "sha256:adf28099d061a25fbcc6531febb7a091e027605385de9fe14dd6a97319d614cf", + "sha256:afa01d25769af33a8dac0d905d5c7bb2d73c7c3d5161b2dd6f8b5b5eea6a3c4c", + "sha256:b1fc07896fc1851558f532dffc8987e526b682ec73140886c831d773cef44b76", + "sha256:b49c604ace7a7aa8af31196abbf8f2193be605db6739ed905ecaf62af31ccae0", + "sha256:b9f3e0bffad6e238f7acc20c393c1ed8fab4371e3b3bc311020dfa6020d99212", + "sha256:ba07646f35e4e49376c9831130039d1b478fbfa1215ae62ad62d2ee63cf9c18f", + "sha256:bd88f40f2294440d3f3c6308e50d96a0d3d0973d6f1a5732875d10f569acef49", + "sha256:c0be58529d43d38ae849a91932391eb93275a06b93b79a8ab828b012e916a206", + "sha256:c45f62e4107ebd05166717ac58f6feb44471ed450d07fecd90e5f69d9bf03c48", + "sha256:c56da23034fe66221f2208c813d8aa509eea34d97328ce2add56e219c3a9f41c", + "sha256:c94b5537bf6ce66e4d7830c6993152940a188600f6ae044435287753044a8fe2", + "sha256:cebf8d56fee3b08ad40d332a807ecccd4153d3f1ba8231e111d9759f02edfd05", + "sha256:d0bf6f93a55d3fa7a079d811b29100b019784e2ee6bc06b0bb839538272a5610", + "sha256:d195add190abccefc70ad0f9a0141ad7da53e16183048380e688b466702195dd", + "sha256:d25ef0c33f22649b7a088035fd65ac1ce6464fa2876578df1adad9472f918a76", + "sha256:d6cbdf12ef967a6aa401cf5cdf47850559e59eedad10e781471c960583f25aa1", + "sha256:d8c032ccee90b37b44e05948b449a2d6baed7e614df3d3f47fe432c952c21b60", + "sha256:daff04257b49ab7f4b3f73f98283d3dbb1a65bf3500d55c7beac3c66c310fe34", + "sha256:e83ebbf020be727d6e0991c1b192a5c2e7113eb66e3def0cd0c62f9f266247e4", + "sha256:ed3025a8a7e5a59817b7494686d449ebfbe301f3e757b852c8d0d1961d6be864", + "sha256:f1936ef138bed2165dd8573aa65e3095ef7c2b6247faccd0e15186aabdda7f66", + "sha256:f5247a3d74355f8b1d780d0f3b32a23dd9f6d3ff43ef2037c6dcd249f35ecf4c", + "sha256:fa496cd45cda0165d597e9d6f01e36c33c9508f75cf03c0a650018c5048f578e", + "sha256:fb4363e6c9fc87365c2bc777a1f585a22f2f56642501885ffc7942138499bf54", + "sha256:fb4370b15111905bf8b5ba2129b926af9470f014cb0493a67d23e9d7a48348e8", + "sha256:fbec2af0ebafa57eb82c18c304b37c86a8abddf7022955d1742b3d5471a6339e" ], - "markers": "python_version >= '3.7'", - "version": "==2.14.6" + "markers": "python_version >= '3.8'", + "version": "==2.16.1" }, "pyyaml": { "hashes": [ @@ -313,11 +291,11 @@ }, "urllib3": { "hashes": [ - "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", - "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54" + "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20", + "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224" ], "index": "pypi", - "version": "==2.1.0" + "version": "==2.2.0" } }, "develop": { @@ -339,31 +317,31 @@ }, "black": { "hashes": [ - "sha256:0cd59d01bf3306ff7e3076dd7f4435fcd2fafe5506a6111cae1138fc7de52382", - "sha256:1e0fa70b8464055069864a4733901b31cbdbe1273f63a24d2fa9d726723d45ac", - "sha256:30fbf768cd4f4576598b1db0202413fafea9a227ef808d1a12230c643cefe9fc", - "sha256:39addf23f7070dbc0b5518cdb2018468ac249d7412a669b50ccca18427dba1f3", - "sha256:5134a6f6b683aa0a5592e3fd61dd3519d8acd953d93e2b8b76f9981245b65594", - "sha256:6a8977774929b5db90442729f131221e58cc5d8208023c6af9110f26f75b6b20", - "sha256:6cc5a6ba3e671cfea95a40030b16a98ee7dc2e22b6427a6f3389567ecf1b5262", - "sha256:780f13d03066a7daf1707ec723fdb36bd698ffa29d95a2e7ef33a8dd8fe43b5c", - "sha256:7fa8d9aaa22d846f8c0f7f07391148e5e346562e9b215794f9101a8339d8b6d8", - "sha256:827a7c0da520dd2f8e6d7d3595f4591aa62ccccce95b16c0e94bb4066374c4c2", - "sha256:82d9452aeabd51d1c8f0d52d4d18e82b9f010ecb30fd55867b5ff95904f427ff", - "sha256:94d5280d020dadfafc75d7cae899609ed38653d3f5e82e7ce58f75e76387ed3d", - "sha256:9aede09f72b2a466e673ee9fca96e4bccc36f463cac28a35ce741f0fd13aea8b", - "sha256:a15670c650668399c4b5eae32e222728185961d6ef6b568f62c1681d57b381ba", - "sha256:a5a0100b4bdb3744dd68412c3789f472d822dc058bb3857743342f8d7f93a5a7", - "sha256:aaf9aa85aaaa466bf969e7dd259547f4481b712fe7ee14befeecc152c403ee05", - "sha256:be305563ff4a2dea813f699daaffac60b977935f3264f66922b1936a5e492ee4", - "sha256:bf8dd261ee82df1abfb591f97e174345ab7375a55019cc93ad38993b9ff5c6ad", - "sha256:d74d4d0da276fbe3b95aa1f404182562c28a04402e4ece60cf373d0b902f33a0", - "sha256:e0e367759062dcabcd9a426d12450c6d61faf1704a352a49055a04c9f9ce8f5a", - "sha256:ec489cae76eac3f7573629955573c3a0e913641cafb9e3bfc87d8ce155ebdb29", - "sha256:f0dfbfbacfbf9cd1fac7a5ddd3e72510ffa93e841a69fcf4a6358feab1685382" + "sha256:0269dfdea12442022e88043d2910429bed717b2d04523867a85dacce535916b8", + "sha256:07204d078e25327aad9ed2c64790d681238686bce254c910de640c7cc4fc3aa6", + "sha256:08b34e85170d368c37ca7bf81cf67ac863c9d1963b2c1780c39102187ec8dd62", + "sha256:1a95915c98d6e32ca43809d46d932e2abc5f1f7d582ffbe65a5b4d1588af7445", + "sha256:2588021038bd5ada078de606f2a804cadd0a3cc6a79cb3e9bb3a8bf581325a4c", + "sha256:2fa6a0e965779c8f2afb286f9ef798df770ba2b6cee063c650b96adec22c056a", + "sha256:34afe9da5056aa123b8bfda1664bfe6fb4e9c6f311d8e4a6eb089da9a9173bf9", + "sha256:3897ae5a21ca132efa219c029cce5e6bfc9c3d34ed7e892113d199c0b1b444a2", + "sha256:40657e1b78212d582a0edecafef133cf1dd02e6677f539b669db4746150d38f6", + "sha256:48b5760dcbfe5cf97fd4fba23946681f3a81514c6ab8a45b50da67ac8fbc6c7b", + "sha256:5242ecd9e990aeb995b6d03dc3b2d112d4a78f2083e5a8e86d566340ae80fec4", + "sha256:5cdc2e2195212208fbcae579b931407c1fa9997584f0a415421748aeafff1168", + "sha256:5d7b06ea8816cbd4becfe5f70accae953c53c0e53aa98730ceccb0395520ee5d", + "sha256:7258c27115c1e3b5de9ac6c4f9957e3ee2c02c0b39222a24dc7aa03ba0e986f5", + "sha256:854c06fb86fd854140f37fb24dbf10621f5dab9e3b0c29a690ba595e3d543024", + "sha256:a21725862d0e855ae05da1dd25e3825ed712eaaccef6b03017fe0853a01aa45e", + "sha256:a83fe522d9698d8f9a101b860b1ee154c1d25f8a82ceb807d319f085b2627c5b", + "sha256:b3d64db762eae4a5ce04b6e3dd745dcca0fb9560eb931a5be97472e38652a161", + "sha256:e298d588744efda02379521a19639ebcd314fba7a49be22136204d7ed1782717", + "sha256:e2c8dfa14677f90d976f68e0c923947ae68fa3961d61ee30976c388adc0b02c8", + "sha256:ecba2a15dfb2d97105be74bbfe5128bc5e9fa8477d8c46766505c1dda5883aac", + "sha256:fc1ec9aa6f4d98d022101e015261c056ddebe3da6a8ccfc2c792cbe0349d48b7" ], "index": "pypi", - "version": "==24.1.0" + "version": "==24.1.1" }, "build": { "hashes": [ @@ -441,11 +419,11 @@ }, "dill": { "hashes": [ - "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e", - "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03" + "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca", + "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7" ], "markers": "python_version >= '3.11'", - "version": "==0.3.7" + "version": "==0.3.8" }, "editables": { "hashes": [ @@ -534,69 +512,69 @@ }, "markupsafe": { "hashes": [ - "sha256:0042d6a9880b38e1dd9ff83146cc3c9c18a059b9360ceae207805567aacccc69", - "sha256:0c26f67b3fe27302d3a412b85ef696792c4a2386293c53ba683a89562f9399b0", - "sha256:0fbad3d346df8f9d72622ac71b69565e621ada2ce6572f37c2eae8dacd60385d", - "sha256:15866d7f2dc60cfdde12ebb4e75e41be862348b4728300c36cdf405e258415ec", - "sha256:1c98c33ffe20e9a489145d97070a435ea0679fddaabcafe19982fe9c971987d5", - "sha256:21e7af8091007bf4bebf4521184f4880a6acab8df0df52ef9e513d8e5db23411", - "sha256:23984d1bdae01bee794267424af55eef4dfc038dc5d1272860669b2aa025c9e3", - "sha256:31f57d64c336b8ccb1966d156932f3daa4fee74176b0fdc48ef580be774aae74", - "sha256:3583a3a3ab7958e354dc1d25be74aee6228938312ee875a22330c4dc2e41beb0", - "sha256:36d7626a8cca4d34216875aee5a1d3d654bb3dac201c1c003d182283e3205949", - "sha256:396549cea79e8ca4ba65525470d534e8a41070e6b3500ce2414921099cb73e8d", - "sha256:3a66c36a3864df95e4f62f9167c734b3b1192cb0851b43d7cc08040c074c6279", - "sha256:3aae9af4cac263007fd6309c64c6ab4506dd2b79382d9d19a1994f9240b8db4f", - "sha256:3ab3a886a237f6e9c9f4f7d272067e712cdb4efa774bef494dccad08f39d8ae6", - "sha256:47bb5f0142b8b64ed1399b6b60f700a580335c8e1c57f2f15587bd072012decc", - "sha256:49a3b78a5af63ec10d8604180380c13dcd870aba7928c1fe04e881d5c792dc4e", - "sha256:4df98d4a9cd6a88d6a585852f56f2155c9cdb6aec78361a19f938810aa020954", - "sha256:5045e892cfdaecc5b4c01822f353cf2c8feb88a6ec1c0adef2a2e705eef0f656", - "sha256:5244324676254697fe5c181fc762284e2c5fceeb1c4e3e7f6aca2b6f107e60dc", - "sha256:54635102ba3cf5da26eb6f96c4b8c53af8a9c0d97b64bdcb592596a6255d8518", - "sha256:54a7e1380dfece8847c71bf7e33da5d084e9b889c75eca19100ef98027bd9f56", - "sha256:55d03fea4c4e9fd0ad75dc2e7e2b6757b80c152c032ea1d1de487461d8140efc", - "sha256:698e84142f3f884114ea8cf83e7a67ca8f4ace8454e78fe960646c6c91c63bfa", - "sha256:6aa5e2e7fc9bc042ae82d8b79d795b9a62bd8f15ba1e7594e3db243f158b5565", - "sha256:7653fa39578957bc42e5ebc15cf4361d9e0ee4b702d7d5ec96cdac860953c5b4", - "sha256:765f036a3d00395a326df2835d8f86b637dbaf9832f90f5d196c3b8a7a5080cb", - "sha256:78bc995e004681246e85e28e068111a4c3f35f34e6c62da1471e844ee1446250", - "sha256:7a07f40ef8f0fbc5ef1000d0c78771f4d5ca03b4953fc162749772916b298fc4", - "sha256:8b570a1537367b52396e53325769608f2a687ec9a4363647af1cded8928af959", - "sha256:987d13fe1d23e12a66ca2073b8d2e2a75cec2ecb8eab43ff5624ba0ad42764bc", - "sha256:9896fca4a8eb246defc8b2a7ac77ef7553b638e04fbf170bff78a40fa8a91474", - "sha256:9e9e3c4020aa2dc62d5dd6743a69e399ce3de58320522948af6140ac959ab863", - "sha256:a0b838c37ba596fcbfca71651a104a611543077156cb0a26fe0c475e1f152ee8", - "sha256:a4d176cfdfde84f732c4a53109b293d05883e952bbba68b857ae446fa3119b4f", - "sha256:a76055d5cb1c23485d7ddae533229039b850db711c554a12ea64a0fd8a0129e2", - "sha256:a76cd37d229fc385738bd1ce4cba2a121cf26b53864c1772694ad0ad348e509e", - "sha256:a7cc49ef48a3c7a0005a949f3c04f8baa5409d3f663a1b36f0eba9bfe2a0396e", - "sha256:abf5ebbec056817057bfafc0445916bb688a255a5146f900445d081db08cbabb", - "sha256:b0fe73bac2fed83839dbdbe6da84ae2a31c11cfc1c777a40dbd8ac8a6ed1560f", - "sha256:b6f14a9cd50c3cb100eb94b3273131c80d102e19bb20253ac7bd7336118a673a", - "sha256:b83041cda633871572f0d3c41dddd5582ad7d22f65a72eacd8d3d6d00291df26", - "sha256:b835aba863195269ea358cecc21b400276747cc977492319fd7682b8cd2c253d", - "sha256:bf1196dcc239e608605b716e7b166eb5faf4bc192f8a44b81e85251e62584bd2", - "sha256:c669391319973e49a7c6230c218a1e3044710bc1ce4c8e6eb71f7e6d43a2c131", - "sha256:c7556bafeaa0a50e2fe7dc86e0382dea349ebcad8f010d5a7dc6ba568eaaa789", - "sha256:c8f253a84dbd2c63c19590fa86a032ef3d8cc18923b8049d91bcdeeb2581fbf6", - "sha256:d18b66fe626ac412d96c2ab536306c736c66cf2a31c243a45025156cc190dc8a", - "sha256:d5291d98cd3ad9a562883468c690a2a238c4a6388ab3bd155b0c75dd55ece858", - "sha256:d5c31fe855c77cad679b302aabc42d724ed87c043b1432d457f4976add1c2c3e", - "sha256:d6e427c7378c7f1b2bef6a344c925b8b63623d3321c09a237b7cc0e77dd98ceb", - "sha256:dac1ebf6983148b45b5fa48593950f90ed6d1d26300604f321c74a9ca1609f8e", - "sha256:de8153a7aae3835484ac168a9a9bdaa0c5eee4e0bc595503c95d53b942879c84", - "sha256:e1a0d1924a5013d4f294087e00024ad25668234569289650929ab871231668e7", - "sha256:e7902211afd0af05fbadcc9a312e4cf10f27b779cf1323e78d52377ae4b72bea", - "sha256:e888ff76ceb39601c59e219f281466c6d7e66bd375b4ec1ce83bcdc68306796b", - "sha256:f06e5a9e99b7df44640767842f414ed5d7bedaaa78cd817ce04bbd6fd86e2dd6", - "sha256:f6be2d708a9d0e9b0054856f07ac7070fbe1754be40ca8525d5adccdbda8f475", - "sha256:f9917691f410a2e0897d1ef99619fd3f7dd503647c8ff2475bf90c3cf222ad74", - "sha256:fc1a75aa8f11b87910ffd98de62b29d6520b6d6e8a3de69a70ca34dea85d2a8a", - "sha256:fe8512ed897d5daf089e5bd010c3dc03bb1bdae00b35588c49b98268d4a01e00" + "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", + "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", + "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", + "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", + "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", + "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", + "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", + "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df", + "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", + "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", + "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", + "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", + "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", + "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371", + "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2", + "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", + "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52", + "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", + "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", + "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", + "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", + "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", + "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", + "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", + "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", + "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", + "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", + "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", + "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", + "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9", + "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", + "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", + "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", + "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", + "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", + "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", + "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a", + "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", + "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", + "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", + "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", + "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", + "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", + "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", + "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", + "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f", + "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50", + "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", + "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", + "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", + "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", + "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", + "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", + "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", + "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf", + "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", + "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", + "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", + "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", + "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68" ], "markers": "python_version >= '3.7'", - "version": "==2.1.4" + "version": "==2.1.5" }, "mccabe": { "hashes": [ @@ -660,11 +638,11 @@ }, "platformdirs": { "hashes": [ - "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380", - "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420" + "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", + "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768" ], "markers": "python_version >= '3.8'", - "version": "==4.1.0" + "version": "==4.2.0" }, "pluggy": { "hashes": [ @@ -716,11 +694,11 @@ }, "pytest": { "hashes": [ - "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", - "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8" + "sha256:249b1b0864530ba251b7438274c4d251c58d868edaaec8762893ad4a0d71c36c", + "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6" ], "index": "pypi", - "version": "==7.4.4" + "version": "==8.0.0" }, "pytest-cov": { "hashes": [ @@ -832,10 +810,10 @@ }, "trove-classifiers": { "hashes": [ - "sha256:3c1ff4deb10149c7e39ede6e5bbc107def64362ef1ee7590ec98d71fb92f1b6a", - "sha256:6e36caf430ff6485c4b57a4c6b364a13f6a898d16b9417c6c37467e59c14b05a" + "sha256:854aba3358f3cf10e5c0916aa533f5a39e27aadd8ade26a54cdc2a93257e39c4", + "sha256:bfdfe60bbf64985c524416afb637ecc79c558e0beb4b7f52b0039e01044b0229" ], - "version": "==2024.1.8" + "version": "==2024.1.31" }, "typing-extensions": { "hashes": [ diff --git a/lint-workflow-v2/flake.lock b/lint-workflow-v2/flake.lock index adbaed05..27363ca4 100644 --- a/lint-workflow-v2/flake.lock +++ b/lint-workflow-v2/flake.lock @@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1701680307, - "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", + "lastModified": 1705309234, + "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", "owner": "numtide", "repo": "flake-utils", - "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", + "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", "type": "github" }, "original": { @@ -19,16 +19,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1669833724, - "narHash": "sha256-/HEZNyGbnQecrgJnfE8d0WC5c1xuPSD2LUpB6YXlg4c=", + "lastModified": 1706683685, + "narHash": "sha256-FtPPshEpxH/ewBOsdKBNhlsL2MLEFv1hEnQ19f/bFsQ=", "owner": "nixos", "repo": "nixpkgs", - "rev": "4d2b37a84fad1091b9de401eb450aae66f1a741e", + "rev": "5ad9903c16126a7d949101687af0aa589b1d7d3d", "type": "github" }, "original": { "owner": "nixos", - "ref": "22.11", + "ref": "nixpkgs-unstable", "repo": "nixpkgs", "type": "github" } diff --git a/lint-workflow-v2/flake.nix b/lint-workflow-v2/flake.nix index 3ed8e30a..f8c0a61b 100644 --- a/lint-workflow-v2/flake.nix +++ b/lint-workflow-v2/flake.nix @@ -1,6 +1,6 @@ { description = "GitHub Action Linter"; - inputs = { nixpkgs.url = "github:nixos/nixpkgs/22.11"; }; + inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; }; outputs = { self, nixpkgs, flake-utils}: flake-utils.lib.eachDefaultSystem (system: diff --git a/lint-workflow-v2/pyproject.toml b/lint-workflow-v2/pyproject.toml index 2a6b9ac7..a6bfb0ca 100644 --- a/lint-workflow-v2/pyproject.toml +++ b/lint-workflow-v2/pyproject.toml @@ -1,9 +1,11 @@ [build-system] +#requires = ["setuptools>=61.0"] +#build-backend = "setuptools.build_meta" requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "bitwarden-workflow-linter" +name = "bitwarden_workflow_linter" dynamic = ["version"] authors = [] description = "Custom GitHub Action Workflow Linter" @@ -22,6 +24,13 @@ Issues = "https://github.com/bitwarden/gh-actions/issues" [project.scripts] bwwl = "src.cli:main" +#[tool.setuptools] +#packages = ["src"] +# +#[tool.setuptools.package-data] +#"*" = ["*.json"] + + [tool.hatch.build.targets.wheel] packages = ["src"] From 78d55615adc3d3ea0cea24781ab84d0eef837f04 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 2 Feb 2024 11:11:55 -0800 Subject: [PATCH 43/57] Updating file structure. Tests passing --- lint-workflow-v2/Pipfile | 1 - lint-workflow-v2/Pipfile.lock | 6 +----- .../src/{ => bitwarden_workflow_linter}/__about__.py | 0 .../src/{ => bitwarden_workflow_linter}/__init__.py | 0 .../src/{ => bitwarden_workflow_linter}/actions.py | 2 +- .../src/{ => bitwarden_workflow_linter}/cli.py | 0 .../default_actions.json | 0 .../default_settings.py | 0 .../src/{ => bitwarden_workflow_linter}/lint.py | 4 ++-- .../src/{ => bitwarden_workflow_linter}/load.py | 0 .../{ => bitwarden_workflow_linter}/models/__init__.py | 0 .../src/{ => bitwarden_workflow_linter}/models/job.py | 2 +- .../src/{ => bitwarden_workflow_linter}/models/step.py | 0 .../{ => bitwarden_workflow_linter}/models/workflow.py | 2 +- .../src/{ => bitwarden_workflow_linter}/rule.py | 0 .../{ => bitwarden_workflow_linter}/rules/__init__.py | 0 .../rules/job_environment_prefix.py | 0 .../rules/name_capitalized.py | 0 .../rules/name_exists.py | 0 .../rules/pinned_job_runner.py | 0 .../rules/step_approved.py | 0 .../rules/step_pinned.py | 0 .../src/{ => bitwarden_workflow_linter}/utils.py | 0 .../tests/rules/test_job_environment_prefix.py | 4 ++-- lint-workflow-v2/tests/rules/test_name_capitalized.py | 4 ++-- lint-workflow-v2/tests/rules/test_name_exists.py | 4 ++-- lint-workflow-v2/tests/rules/test_pinned_job_runner.py | 4 ++-- lint-workflow-v2/tests/rules/test_step_approved.py | 6 +++--- lint-workflow-v2/tests/rules/test_step_pinned.py | 4 ++-- lint-workflow-v2/tests/test_job.py | 4 ++-- lint-workflow-v2/tests/test_lint.py | 4 ++-- lint-workflow-v2/tests/test_load.py | 4 ++-- lint-workflow-v2/tests/test_rule.py | 10 +++++----- lint-workflow-v2/tests/test_step.py | 2 +- lint-workflow-v2/tests/test_utils.py | 2 +- lint-workflow-v2/tests/test_workflow.py | 6 +++--- 36 files changed, 35 insertions(+), 40 deletions(-) rename lint-workflow-v2/src/{ => bitwarden_workflow_linter}/__about__.py (100%) rename lint-workflow-v2/src/{ => bitwarden_workflow_linter}/__init__.py (100%) rename lint-workflow-v2/src/{ => bitwarden_workflow_linter}/actions.py (99%) rename lint-workflow-v2/src/{ => bitwarden_workflow_linter}/cli.py (100%) rename lint-workflow-v2/src/{ => bitwarden_workflow_linter}/default_actions.json (100%) rename lint-workflow-v2/src/{ => bitwarden_workflow_linter}/default_settings.py (100%) rename lint-workflow-v2/src/{ => bitwarden_workflow_linter}/lint.py (98%) rename lint-workflow-v2/src/{ => bitwarden_workflow_linter}/load.py (100%) rename lint-workflow-v2/src/{ => bitwarden_workflow_linter}/models/__init__.py (100%) rename lint-workflow-v2/src/{ => bitwarden_workflow_linter}/models/job.py (96%) rename lint-workflow-v2/src/{ => bitwarden_workflow_linter}/models/step.py (100%) rename lint-workflow-v2/src/{ => bitwarden_workflow_linter}/models/workflow.py (96%) rename lint-workflow-v2/src/{ => bitwarden_workflow_linter}/rule.py (100%) rename lint-workflow-v2/src/{ => bitwarden_workflow_linter}/rules/__init__.py (100%) rename lint-workflow-v2/src/{ => bitwarden_workflow_linter}/rules/job_environment_prefix.py (100%) rename lint-workflow-v2/src/{ => bitwarden_workflow_linter}/rules/name_capitalized.py (100%) rename lint-workflow-v2/src/{ => bitwarden_workflow_linter}/rules/name_exists.py (100%) rename lint-workflow-v2/src/{ => bitwarden_workflow_linter}/rules/pinned_job_runner.py (100%) rename lint-workflow-v2/src/{ => bitwarden_workflow_linter}/rules/step_approved.py (100%) rename lint-workflow-v2/src/{ => bitwarden_workflow_linter}/rules/step_pinned.py (100%) rename lint-workflow-v2/src/{ => bitwarden_workflow_linter}/utils.py (100%) diff --git a/lint-workflow-v2/Pipfile b/lint-workflow-v2/Pipfile index 0067d699..8dc6f95b 100644 --- a/lint-workflow-v2/Pipfile +++ b/lint-workflow-v2/Pipfile @@ -9,7 +9,6 @@ urllib3 = "*" pydantic = "*" "ruamel.yaml" = "*" dataclasses-json = "*" -bitwarden-workflow-linter = {editable = true, path = "."} [dev-packages] black = "*" diff --git a/lint-workflow-v2/Pipfile.lock b/lint-workflow-v2/Pipfile.lock index c13b2625..54e4bd9f 100644 --- a/lint-workflow-v2/Pipfile.lock +++ b/lint-workflow-v2/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "bff95bd7882b10ad96449f9b3bdb3e9cfae609496730fa578d9de49ec0c61735" + "sha256": "7e51933f1987a5e1d1182f4cec2a1c6f84194a17ac6653bb3674da0fe26dfb7f" }, "pipfile-spec": 6, "requires": { @@ -24,10 +24,6 @@ "markers": "python_version >= '3.8'", "version": "==0.6.0" }, - "bitwarden-workflow-linter": { - "editable": true, - "path": "." - }, "dataclasses-json": { "hashes": [ "sha256:73696ebf24936560cca79a2430cbc4f3dd23ac7bf46ed17f38e5e5e7657a6377", diff --git a/lint-workflow-v2/src/__about__.py b/lint-workflow-v2/src/bitwarden_workflow_linter/__about__.py similarity index 100% rename from lint-workflow-v2/src/__about__.py rename to lint-workflow-v2/src/bitwarden_workflow_linter/__about__.py diff --git a/lint-workflow-v2/src/__init__.py b/lint-workflow-v2/src/bitwarden_workflow_linter/__init__.py similarity index 100% rename from lint-workflow-v2/src/__init__.py rename to lint-workflow-v2/src/bitwarden_workflow_linter/__init__.py diff --git a/lint-workflow-v2/src/actions.py b/lint-workflow-v2/src/bitwarden_workflow_linter/actions.py similarity index 99% rename from lint-workflow-v2/src/actions.py rename to lint-workflow-v2/src/bitwarden_workflow_linter/actions.py index 74d09ae4..a54af097 100644 --- a/lint-workflow-v2/src/actions.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/actions.py @@ -9,7 +9,7 @@ from dataclasses import asdict from typing import Optional, Tuple, Union -from src.utils import Colors, Settings, Action +from src.bitwarden_workflow_linter.utils import Colors, Settings, Action class ActionsCmd: diff --git a/lint-workflow-v2/src/cli.py b/lint-workflow-v2/src/bitwarden_workflow_linter/cli.py similarity index 100% rename from lint-workflow-v2/src/cli.py rename to lint-workflow-v2/src/bitwarden_workflow_linter/cli.py diff --git a/lint-workflow-v2/src/default_actions.json b/lint-workflow-v2/src/bitwarden_workflow_linter/default_actions.json similarity index 100% rename from lint-workflow-v2/src/default_actions.json rename to lint-workflow-v2/src/bitwarden_workflow_linter/default_actions.json diff --git a/lint-workflow-v2/src/default_settings.py b/lint-workflow-v2/src/bitwarden_workflow_linter/default_settings.py similarity index 100% rename from lint-workflow-v2/src/default_settings.py rename to lint-workflow-v2/src/bitwarden_workflow_linter/default_settings.py diff --git a/lint-workflow-v2/src/lint.py b/lint-workflow-v2/src/bitwarden_workflow_linter/lint.py similarity index 98% rename from lint-workflow-v2/src/lint.py rename to lint-workflow-v2/src/bitwarden_workflow_linter/lint.py index 8cf24344..1a56fd37 100644 --- a/lint-workflow-v2/src/lint.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/lint.py @@ -7,8 +7,8 @@ from functools import reduce from typing import Optional -from src.load import WorkflowBuilder, Rules -from src.utils import LintFinding, Settings +from .load import WorkflowBuilder, Rules +from .utils import LintFinding, Settings class LinterCmd: diff --git a/lint-workflow-v2/src/load.py b/lint-workflow-v2/src/bitwarden_workflow_linter/load.py similarity index 100% rename from lint-workflow-v2/src/load.py rename to lint-workflow-v2/src/bitwarden_workflow_linter/load.py diff --git a/lint-workflow-v2/src/models/__init__.py b/lint-workflow-v2/src/bitwarden_workflow_linter/models/__init__.py similarity index 100% rename from lint-workflow-v2/src/models/__init__.py rename to lint-workflow-v2/src/bitwarden_workflow_linter/models/__init__.py diff --git a/lint-workflow-v2/src/models/job.py b/lint-workflow-v2/src/bitwarden_workflow_linter/models/job.py similarity index 96% rename from lint-workflow-v2/src/models/job.py rename to lint-workflow-v2/src/bitwarden_workflow_linter/models/job.py index c880bfc0..861ea451 100644 --- a/lint-workflow-v2/src/models/job.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/models/job.py @@ -6,7 +6,7 @@ from dataclasses_json import config, dataclass_json, Undefined from ruamel.yaml.comments import CommentedMap -from src.models.step import Step +from .step import Step @dataclass_json(undefined=Undefined.EXCLUDE) diff --git a/lint-workflow-v2/src/models/step.py b/lint-workflow-v2/src/bitwarden_workflow_linter/models/step.py similarity index 100% rename from lint-workflow-v2/src/models/step.py rename to lint-workflow-v2/src/bitwarden_workflow_linter/models/step.py diff --git a/lint-workflow-v2/src/models/workflow.py b/lint-workflow-v2/src/bitwarden_workflow_linter/models/workflow.py similarity index 96% rename from lint-workflow-v2/src/models/workflow.py rename to lint-workflow-v2/src/bitwarden_workflow_linter/models/workflow.py index 45733e68..41b84102 100644 --- a/lint-workflow-v2/src/models/workflow.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/models/workflow.py @@ -6,7 +6,7 @@ from dataclasses_json import dataclass_json, Undefined from ruamel.yaml.comments import CommentedMap -from src.models.job import Job +from .job import Job @dataclass_json(undefined=Undefined.EXCLUDE) diff --git a/lint-workflow-v2/src/rule.py b/lint-workflow-v2/src/bitwarden_workflow_linter/rule.py similarity index 100% rename from lint-workflow-v2/src/rule.py rename to lint-workflow-v2/src/bitwarden_workflow_linter/rule.py diff --git a/lint-workflow-v2/src/rules/__init__.py b/lint-workflow-v2/src/bitwarden_workflow_linter/rules/__init__.py similarity index 100% rename from lint-workflow-v2/src/rules/__init__.py rename to lint-workflow-v2/src/bitwarden_workflow_linter/rules/__init__.py diff --git a/lint-workflow-v2/src/rules/job_environment_prefix.py b/lint-workflow-v2/src/bitwarden_workflow_linter/rules/job_environment_prefix.py similarity index 100% rename from lint-workflow-v2/src/rules/job_environment_prefix.py rename to lint-workflow-v2/src/bitwarden_workflow_linter/rules/job_environment_prefix.py diff --git a/lint-workflow-v2/src/rules/name_capitalized.py b/lint-workflow-v2/src/bitwarden_workflow_linter/rules/name_capitalized.py similarity index 100% rename from lint-workflow-v2/src/rules/name_capitalized.py rename to lint-workflow-v2/src/bitwarden_workflow_linter/rules/name_capitalized.py diff --git a/lint-workflow-v2/src/rules/name_exists.py b/lint-workflow-v2/src/bitwarden_workflow_linter/rules/name_exists.py similarity index 100% rename from lint-workflow-v2/src/rules/name_exists.py rename to lint-workflow-v2/src/bitwarden_workflow_linter/rules/name_exists.py diff --git a/lint-workflow-v2/src/rules/pinned_job_runner.py b/lint-workflow-v2/src/bitwarden_workflow_linter/rules/pinned_job_runner.py similarity index 100% rename from lint-workflow-v2/src/rules/pinned_job_runner.py rename to lint-workflow-v2/src/bitwarden_workflow_linter/rules/pinned_job_runner.py diff --git a/lint-workflow-v2/src/rules/step_approved.py b/lint-workflow-v2/src/bitwarden_workflow_linter/rules/step_approved.py similarity index 100% rename from lint-workflow-v2/src/rules/step_approved.py rename to lint-workflow-v2/src/bitwarden_workflow_linter/rules/step_approved.py diff --git a/lint-workflow-v2/src/rules/step_pinned.py b/lint-workflow-v2/src/bitwarden_workflow_linter/rules/step_pinned.py similarity index 100% rename from lint-workflow-v2/src/rules/step_pinned.py rename to lint-workflow-v2/src/bitwarden_workflow_linter/rules/step_pinned.py diff --git a/lint-workflow-v2/src/utils.py b/lint-workflow-v2/src/bitwarden_workflow_linter/utils.py similarity index 100% rename from lint-workflow-v2/src/utils.py rename to lint-workflow-v2/src/bitwarden_workflow_linter/utils.py diff --git a/lint-workflow-v2/tests/rules/test_job_environment_prefix.py b/lint-workflow-v2/tests/rules/test_job_environment_prefix.py index 449c87c5..4d92eeda 100644 --- a/lint-workflow-v2/tests/rules/test_job_environment_prefix.py +++ b/lint-workflow-v2/tests/rules/test_job_environment_prefix.py @@ -4,8 +4,8 @@ from ruamel.yaml import YAML -from src.load import WorkflowBuilder -from src.rules.job_environment_prefix import RuleJobEnvironmentPrefix +from src.bitwarden_workflow_linter.load import WorkflowBuilder +from src.bitwarden_workflow_linter.rules.job_environment_prefix import RuleJobEnvironmentPrefix yaml = YAML() diff --git a/lint-workflow-v2/tests/rules/test_name_capitalized.py b/lint-workflow-v2/tests/rules/test_name_capitalized.py index 45d01498..6317987f 100644 --- a/lint-workflow-v2/tests/rules/test_name_capitalized.py +++ b/lint-workflow-v2/tests/rules/test_name_capitalized.py @@ -4,8 +4,8 @@ from ruamel.yaml import YAML -from src.load import WorkflowBuilder -from src.rules.name_capitalized import RuleNameCapitalized +from src.bitwarden_workflow_linter.load import WorkflowBuilder +from src.bitwarden_workflow_linter.rules.name_capitalized import RuleNameCapitalized yaml = YAML() diff --git a/lint-workflow-v2/tests/rules/test_name_exists.py b/lint-workflow-v2/tests/rules/test_name_exists.py index 4ca074a2..935acc79 100644 --- a/lint-workflow-v2/tests/rules/test_name_exists.py +++ b/lint-workflow-v2/tests/rules/test_name_exists.py @@ -4,8 +4,8 @@ from ruamel.yaml import YAML -from src.load import WorkflowBuilder -from src.rules.name_exists import RuleNameExists +from src.bitwarden_workflow_linter.load import WorkflowBuilder +from src.bitwarden_workflow_linter.rules.name_exists import RuleNameExists yaml = YAML() diff --git a/lint-workflow-v2/tests/rules/test_pinned_job_runner.py b/lint-workflow-v2/tests/rules/test_pinned_job_runner.py index 0b24c722..89b83508 100644 --- a/lint-workflow-v2/tests/rules/test_pinned_job_runner.py +++ b/lint-workflow-v2/tests/rules/test_pinned_job_runner.py @@ -4,8 +4,8 @@ from ruamel.yaml import YAML -from src.load import WorkflowBuilder -from src.rules.pinned_job_runner import RuleJobRunnerVersionPinned +from src.bitwarden_workflow_linter.load import WorkflowBuilder +from src.bitwarden_workflow_linter.rules.pinned_job_runner import RuleJobRunnerVersionPinned yaml = YAML() diff --git a/lint-workflow-v2/tests/rules/test_step_approved.py b/lint-workflow-v2/tests/rules/test_step_approved.py index 6db227ba..efa082e4 100644 --- a/lint-workflow-v2/tests/rules/test_step_approved.py +++ b/lint-workflow-v2/tests/rules/test_step_approved.py @@ -4,9 +4,9 @@ from ruamel.yaml import YAML -from src.load import WorkflowBuilder -from src.rules.step_approved import RuleStepUsesApproved -from src.utils import Settings +from src.bitwarden_workflow_linter.load import WorkflowBuilder +from src.bitwarden_workflow_linter.rules.step_approved import RuleStepUsesApproved +from src.bitwarden_workflow_linter.utils import Settings yaml = YAML() diff --git a/lint-workflow-v2/tests/rules/test_step_pinned.py b/lint-workflow-v2/tests/rules/test_step_pinned.py index c4ad5c41..cb5d7f06 100644 --- a/lint-workflow-v2/tests/rules/test_step_pinned.py +++ b/lint-workflow-v2/tests/rules/test_step_pinned.py @@ -4,8 +4,8 @@ from ruamel.yaml import YAML -from src.load import WorkflowBuilder -from src.rules.step_pinned import RuleStepUsesPinned +from src.bitwarden_workflow_linter.load import WorkflowBuilder +from src.bitwarden_workflow_linter.rules.step_pinned import RuleStepUsesPinned yaml = YAML() diff --git a/lint-workflow-v2/tests/test_job.py b/lint-workflow-v2/tests/test_job.py index 32481cd8..2a307dc5 100644 --- a/lint-workflow-v2/tests/test_job.py +++ b/lint-workflow-v2/tests/test_job.py @@ -2,8 +2,8 @@ import pytest -from src.models.job import Job -from src.models.step import Step +from src.bitwarden_workflow_linter.models.job import Job +from src.bitwarden_workflow_linter.models.step import Step @pytest.fixture(name="default_job_data") diff --git a/lint-workflow-v2/tests/test_lint.py b/lint-workflow-v2/tests/test_lint.py index 3b19f806..fad17139 100644 --- a/lint-workflow-v2/tests/test_lint.py +++ b/lint-workflow-v2/tests/test_lint.py @@ -2,8 +2,8 @@ import pytest -from src.lint import LinterCmd -from src.utils import Settings, LintFinding, LintLevels +from src.bitwarden_workflow_linter.lint import LinterCmd +from src.bitwarden_workflow_linter.utils import Settings, LintFinding, LintLevels @pytest.fixture(name="settings") diff --git a/lint-workflow-v2/tests/test_load.py b/lint-workflow-v2/tests/test_load.py index 02ee473e..beb86c08 100644 --- a/lint-workflow-v2/tests/test_load.py +++ b/lint-workflow-v2/tests/test_load.py @@ -7,8 +7,8 @@ from .conftest import FIXTURE_DIR -from src.load import WorkflowBuilder -from src.models.workflow import Workflow +from src.bitwarden_workflow_linter.load import WorkflowBuilder +from src.bitwarden_workflow_linter.models.workflow import Workflow yaml = YAML() diff --git a/lint-workflow-v2/tests/test_rule.py b/lint-workflow-v2/tests/test_rule.py index 10a1470e..363e2eb8 100644 --- a/lint-workflow-v2/tests/test_rule.py +++ b/lint-workflow-v2/tests/test_rule.py @@ -5,11 +5,11 @@ from ruamel.yaml import YAML -from src.load import WorkflowBuilder -from src.models.job import Job -from src.models.step import Step -from src.models.workflow import Workflow -from src.rule import Rule, RuleExecutionException +from src.bitwarden_workflow_linter.load import WorkflowBuilder +from src.bitwarden_workflow_linter.models.job import Job +from src.bitwarden_workflow_linter.models.step import Step +from src.bitwarden_workflow_linter.models.workflow import Workflow +from src.bitwarden_workflow_linter.rule import Rule, RuleExecutionException yaml = YAML() diff --git a/lint-workflow-v2/tests/test_step.py b/lint-workflow-v2/tests/test_step.py index da2ec383..deda0f9a 100644 --- a/lint-workflow-v2/tests/test_step.py +++ b/lint-workflow-v2/tests/test_step.py @@ -5,7 +5,7 @@ from ruamel.yaml import YAML -from src.models.step import Step +from src.bitwarden_workflow_linter.models.step import Step @pytest.fixture(name="default_step") diff --git a/lint-workflow-v2/tests/test_utils.py b/lint-workflow-v2/tests/test_utils.py index 3ca5333a..b80b9efa 100644 --- a/lint-workflow-v2/tests/test_utils.py +++ b/lint-workflow-v2/tests/test_utils.py @@ -1,6 +1,6 @@ """Tests src/utils.py.""" -from src.utils import Action, Colors, LintFinding, LintLevels +from src.bitwarden_workflow_linter.utils import Action, Colors, LintFinding, LintLevels def test_action_eq(): diff --git a/lint-workflow-v2/tests/test_workflow.py b/lint-workflow-v2/tests/test_workflow.py index 01b04ffb..9d8d5be0 100644 --- a/lint-workflow-v2/tests/test_workflow.py +++ b/lint-workflow-v2/tests/test_workflow.py @@ -2,9 +2,9 @@ import pytest -from src.models.job import Job -from src.models.step import Step -from src.models.workflow import Workflow +from src.bitwarden_workflow_linter.models.job import Job +from src.bitwarden_workflow_linter.models.step import Step +from src.bitwarden_workflow_linter.models.workflow import Workflow @pytest.fixture(name="default_workflow_data") From 2d057f098305904bfc95fa4108672b0336fa2ee5 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 2 Feb 2024 15:09:51 -0800 Subject: [PATCH 44/57] Got to installable and importable module --- lint-workflow-v2/Taskfile.yml | 10 ++++++ lint-workflow-v2/flake.nix | 1 + lint-workflow-v2/pyproject.toml | 32 ++++++++++------- lint-workflow-v2/pyproject.toml.tpl | 34 +++++++++++++++++++ .../bitwarden_workflow_linter/__about__.py | 2 +- .../src/bitwarden_workflow_linter/actions.py | 2 +- .../default_settings.py | 10 +++--- 7 files changed, 72 insertions(+), 19 deletions(-) create mode 100644 lint-workflow-v2/pyproject.toml.tpl diff --git a/lint-workflow-v2/Taskfile.yml b/lint-workflow-v2/Taskfile.yml index dc82f835..9e53e9bf 100644 --- a/lint-workflow-v2/Taskfile.yml +++ b/lint-workflow-v2/Taskfile.yml @@ -18,9 +18,18 @@ tasks: cmds: - pipenv run pytype src + update: + silent: true + cmds: + - | + deps=$(pipenv requirements --exclude-markers | tail -n +2 | awk '{print "\t\""$0"\","}') + export DEPS=$(printf "$deps") + envsubst < pyproject.toml.tpl > pyproject.toml + install: silent: true cmds: + - task: update - pipenv run python -m pip install -e . test:unit: @@ -58,4 +67,5 @@ tasks: dist: silent: true cmds: + - task: update - pipenv run python -m build diff --git a/lint-workflow-v2/flake.nix b/lint-workflow-v2/flake.nix index f8c0a61b..885768e5 100644 --- a/lint-workflow-v2/flake.nix +++ b/lint-workflow-v2/flake.nix @@ -12,6 +12,7 @@ devShells.default = pkgs.mkShell { name = "${packageName}"; buildInputs = [ + pkgs.envsubst pkgs.pipenv pkgs.python311 ]; diff --git a/lint-workflow-v2/pyproject.toml b/lint-workflow-v2/pyproject.toml index a6bfb0ca..7f695bdd 100644 --- a/lint-workflow-v2/pyproject.toml +++ b/lint-workflow-v2/pyproject.toml @@ -1,6 +1,4 @@ [build-system] -#requires = ["setuptools>=61.0"] -#build-backend = "setuptools.build_meta" requires = ["hatchling"] build-backend = "hatchling.build" @@ -17,22 +15,32 @@ classifiers = [ "Operating System :: OS Independent", ] +dependencies = [ + "annotated-types==0.6.0", + "dataclasses-json==0.6.4", + "marshmallow==3.20.2", + "mypy-extensions==1.0.0", + "packaging==23.2", + "pydantic==2.6.0", + "pydantic-core==2.16.1", + "pyyaml==6.0.1", + "ruamel.yaml==0.18.5", + "ruamel.yaml.clib==0.2.8", + "typing-extensions==4.9.0", + "typing-inspect==0.9.0", + "urllib3==2.2.0", +] + [project.urls] Homepage = "https://github.com/bitwarden/gh-actions/tree/main/lint-workflow-v2" Issues = "https://github.com/bitwarden/gh-actions/issues" -[project.scripts] -bwwl = "src.cli:main" - -#[tool.setuptools] -#packages = ["src"] -# -#[tool.setuptools.package-data] -#"*" = ["*.json"] +[project.scripts] +bwwl = "bitwarden_workflow_linter.cli:main" [tool.hatch.build.targets.wheel] -packages = ["src"] +packages = ["src/bitwarden_workflow_linter"] [tool.hatch.version] -path = "src/__about__.py" +path = "src/bitwarden_workflow_linter/__about__.py" diff --git a/lint-workflow-v2/pyproject.toml.tpl b/lint-workflow-v2/pyproject.toml.tpl new file mode 100644 index 00000000..cb3a2601 --- /dev/null +++ b/lint-workflow-v2/pyproject.toml.tpl @@ -0,0 +1,34 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "bitwarden_workflow_linter" +dynamic = ["version"] +authors = [] +description = "Custom GitHub Action Workflow Linter" +readme = "README.md" +requires-python = ">=3.11" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Operating System :: OS Independent", +] + +dependencies = [ +$DEPS +] + +[project.urls] +Homepage = "https://github.com/bitwarden/gh-actions/tree/main/lint-workflow-v2" +Issues = "https://github.com/bitwarden/gh-actions/issues" + + +[project.scripts] +bwwl = "bitwarden_workflow_linter.cli:main" + +[tool.hatch.build.targets.wheel] +packages = ["src/bitwarden_workflow_linter"] + +[tool.hatch.version] +path = "src/bitwarden_workflow_linter/__about__.py" diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/__about__.py b/lint-workflow-v2/src/bitwarden_workflow_linter/__about__.py index 5820a729..cf04467c 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/__about__.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/__about__.py @@ -1 +1 @@ -version = "0.0.1" +version = "0.0.3" diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/actions.py b/lint-workflow-v2/src/bitwarden_workflow_linter/actions.py index a54af097..f62456a0 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/actions.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/actions.py @@ -9,7 +9,7 @@ from dataclasses import asdict from typing import Optional, Tuple, Union -from src.bitwarden_workflow_linter.utils import Colors, Settings, Action +from .utils import Colors, Settings, Action class ActionsCmd: diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/default_settings.py b/lint-workflow-v2/src/bitwarden_workflow_linter/default_settings.py index 272c3316..2f67be83 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/default_settings.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/default_settings.py @@ -4,11 +4,11 @@ enabled_rules = [ - "src.rules.name_exists.RuleNameExists", - "src.rules.name_capitalized.RuleNameCapitalized", - "src.rules.pinned_job_runner.RuleJobRunnerVersionPinned", - "src.rules.job_environment_prefix.RuleJobEnvironmentPrefix", - "src.rules.step_pinned.RuleStepUsesPinned", + "bitwarden_workflow_linter.rules.name_exists.RuleNameExists", + "bitwarden_workflow_linter.rules.name_capitalized.RuleNameCapitalized", + "bitwarden_workflow_linter.rules.pinned_job_runner.RuleJobRunnerVersionPinned", + "bitwarden_workflow_linter.rules.job_environment_prefix.RuleJobEnvironmentPrefix", + "bitwarden_workflow_linter.rules.step_pinned.RuleStepUsesPinned", ] From 9ddbb7a919ab9c275f47986550d8ce80ced8c3ec Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 23 Feb 2024 14:25:16 -0800 Subject: [PATCH 45/57] Switch setting from a python file to a yaml file --- lint-workflow-v2/settings.py | 18 ----- lint-workflow-v2/settings.yaml | 8 +++ .../default_settings.py | 20 ------ .../default_settings.yaml | 8 +++ .../src/bitwarden_workflow_linter/load.py | 1 + .../src/bitwarden_workflow_linter/utils.py | 65 +++++++++++-------- .../rules/test_job_environment_prefix.py | 4 +- .../tests/rules/test_pinned_job_runner.py | 4 +- 8 files changed, 62 insertions(+), 66 deletions(-) delete mode 100644 lint-workflow-v2/settings.py create mode 100644 lint-workflow-v2/settings.yaml delete mode 100644 lint-workflow-v2/src/bitwarden_workflow_linter/default_settings.py create mode 100644 lint-workflow-v2/src/bitwarden_workflow_linter/default_settings.yaml diff --git a/lint-workflow-v2/settings.py b/lint-workflow-v2/settings.py deleted file mode 100644 index f3f3f627..00000000 --- a/lint-workflow-v2/settings.py +++ /dev/null @@ -1,18 +0,0 @@ -"""This psuedo-module is essentially configuration-as-code.""" - -import json - - -enabled_rules = [ - "src.rules.name_exists.RuleNameExists", - "src.rules.name_capitalized.RuleNameCapitalized", - "src.rules.pinned_job_runner.RuleJobRunnerVersionPinned", - "src.rules.job_environment_prefix.RuleJobEnvironmentPrefix", - "src.rules.step_pinned.RuleStepUsesPinned", -] - - -# with open("actions.json", "r", encoding="utf8") as action_file: -# approved_actions = json.load(action_file) - -# approved_actions_path = "" diff --git a/lint-workflow-v2/settings.yaml b/lint-workflow-v2/settings.yaml new file mode 100644 index 00000000..8b21d5cc --- /dev/null +++ b/lint-workflow-v2/settings.yaml @@ -0,0 +1,8 @@ +enabled_rules: + - bitwarden_workflow_linter.rules.name_exists.RuleNameExists + - bitwarden_workflow_linter.rules.name_capitalized.RuleNameCapitalized + - bitwarden_workflow_linter.rules.pinned_job_runner.RuleJobRunnerVersionPinned + - bitwarden_workflow_linter.rules.job_environment_prefix.RuleJobEnvironmentPrefix + - bitwarden_workflow_linter.rules.step_pinned.RuleStepUsesPinned + +approved_actions_path: default_actions.json diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/default_settings.py b/lint-workflow-v2/src/bitwarden_workflow_linter/default_settings.py deleted file mode 100644 index 2f67be83..00000000 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/default_settings.py +++ /dev/null @@ -1,20 +0,0 @@ -"""This psuedo-module is essentially configuration-as-code.""" - -import json - - -enabled_rules = [ - "bitwarden_workflow_linter.rules.name_exists.RuleNameExists", - "bitwarden_workflow_linter.rules.name_capitalized.RuleNameCapitalized", - "bitwarden_workflow_linter.rules.pinned_job_runner.RuleJobRunnerVersionPinned", - "bitwarden_workflow_linter.rules.job_environment_prefix.RuleJobEnvironmentPrefix", - "bitwarden_workflow_linter.rules.step_pinned.RuleStepUsesPinned", -] - - -# with open("actions.json", "r", encoding="utf8") as action_file: -# approved_actions = json.load(action_file) - -# approved_actions = [] - -approved_actions_path = "default_actions.json" diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/default_settings.yaml b/lint-workflow-v2/src/bitwarden_workflow_linter/default_settings.yaml new file mode 100644 index 00000000..8b21d5cc --- /dev/null +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/default_settings.yaml @@ -0,0 +1,8 @@ +enabled_rules: + - bitwarden_workflow_linter.rules.name_exists.RuleNameExists + - bitwarden_workflow_linter.rules.name_capitalized.RuleNameCapitalized + - bitwarden_workflow_linter.rules.pinned_job_runner.RuleJobRunnerVersionPinned + - bitwarden_workflow_linter.rules.job_environment_prefix.RuleJobEnvironmentPrefix + - bitwarden_workflow_linter.rules.step_pinned.RuleStepUsesPinned + +approved_actions_path: default_actions.json diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/load.py b/lint-workflow-v2/src/bitwarden_workflow_linter/load.py index 6fbcf344..bd87a54a 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/load.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/load.py @@ -1,6 +1,7 @@ """Module to load for Worflows and Rules.""" import importlib + from typing import List, Optional from ruamel.yaml import YAML diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/utils.py b/lint-workflow-v2/src/bitwarden_workflow_linter/utils.py index 0b15b47d..afc4c9e5 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/utils.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/utils.py @@ -3,13 +3,16 @@ import importlib.resources import json import os +import sys from dataclasses import dataclass from enum import Enum from typing import Optional, Self -from .default_settings import enabled_rules as default_enabled_rules -from .default_settings import approved_actions_path as default_approved_actions_path +from ruamel.yaml import YAML + + +yaml = YAML() @dataclass @@ -138,27 +141,37 @@ def __init__( @staticmethod def factory() -> Self: - enabled_rules = default_enabled_rules - approved_actions_file = default_approved_actions_path - - try: - if os.path.exists("settings.py"): - import settings - - if hasattr(settings, "enabled_rules"): - enabled_rules = settings.enabled_rules - - if hasattr(settings, "approved_actions_path"): - if os.path.exists(settings.approved_actions_path): - with open( - settings.approved_actions_path, "r", encoding="utf8" - ) as action_file: - approved_actions = json.load(action_file) - - else: - approved_actions = {} - - except Exception as exc: - raise SettingsError("Error importing settings") from exc - - return Settings(enabled_rules=enabled_rules, approved_actions=approved_actions) + with ( + importlib.resources.files("bitwarden_workflow_linter") + .joinpath("default_settings.yaml") + .open("r", encoding="utf-8") as file + ): + settings = yaml.load(file) + + settings_filename = "settings.yaml" + local_settings = None + + if os.path.exists(settings_filename): + with open(settings_filename, encoding="utf8") as settings_file: + local_settings = yaml.load(settings_file) + + if local_settings: + settings.update(local_settings) + + if settings["approved_actions_path"] == "default_actions.json": + with ( + importlib.resources.files("bitwarden_workflow_linter") + .joinpath("default_actions.json") + .open("r", encoding="utf-8") as file + ): + settings["approved_actions"] = json.load(file) + else: + with open( + settings["approved_actions_path"], "r", encoding="utf8" + ) as action_file: + settings["approved_actions"] = json.load(action_file) + + return Settings( + enabled_rules=settings["enabled_rules"], + approved_actions=settings["approved_actions"], + ) diff --git a/lint-workflow-v2/tests/rules/test_job_environment_prefix.py b/lint-workflow-v2/tests/rules/test_job_environment_prefix.py index 4d92eeda..4c3afbef 100644 --- a/lint-workflow-v2/tests/rules/test_job_environment_prefix.py +++ b/lint-workflow-v2/tests/rules/test_job_environment_prefix.py @@ -5,7 +5,9 @@ from ruamel.yaml import YAML from src.bitwarden_workflow_linter.load import WorkflowBuilder -from src.bitwarden_workflow_linter.rules.job_environment_prefix import RuleJobEnvironmentPrefix +from src.bitwarden_workflow_linter.rules.job_environment_prefix import ( + RuleJobEnvironmentPrefix, +) yaml = YAML() diff --git a/lint-workflow-v2/tests/rules/test_pinned_job_runner.py b/lint-workflow-v2/tests/rules/test_pinned_job_runner.py index 89b83508..b2d6553f 100644 --- a/lint-workflow-v2/tests/rules/test_pinned_job_runner.py +++ b/lint-workflow-v2/tests/rules/test_pinned_job_runner.py @@ -5,7 +5,9 @@ from ruamel.yaml import YAML from src.bitwarden_workflow_linter.load import WorkflowBuilder -from src.bitwarden_workflow_linter.rules.pinned_job_runner import RuleJobRunnerVersionPinned +from src.bitwarden_workflow_linter.rules.pinned_job_runner import ( + RuleJobRunnerVersionPinned, +) yaml = YAML() From 0880b30f7f3ad8102227a18ff5b22592cbdd6cfe Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 23 Feb 2024 14:27:41 -0800 Subject: [PATCH 46/57] Update README with the new yaml settings --- lint-workflow-v2/README.md | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/lint-workflow-v2/README.md b/lint-workflow-v2/README.md index 173b43b6..d9f4cc1e 100644 --- a/lint-workflow-v2/README.md +++ b/lint-workflow-v2/README.md @@ -16,25 +16,20 @@ pip install -e . ``` ## Usage -### Setup settings.py +### Setup settings.yaml -After installation, copy the below and create a `settings.py` in the directory that `bwwl` will be running from. -```python -# settings.py.example -import json - - -enabled_rules = [ - "src.rules.name_exists.RuleNameExists", - "src.rules.name_capitalized.RuleNameCapitalized", - "src.rules.pinned_job_runner.RuleJobRunnerVersionPinned", - "src.rules.job_environment_prefix.RuleJobEnvironmentPrefix", - "src.rules.step_pinned.RuleStepUsesPinned", -] +If a non-default configuration is desired (different than `src/bitwarden_workflow_linter/default_settings.yaml`), copy +the below and create a `settings.yaml` in the directory that `bwwl` will be running from. +```yaml +enabled_rules: + - bitwarden_workflow_linter.rules.name_exists.RuleNameExists + - bitwarden_workflow_linter.rules.name_capitalized.RuleNameCapitalized + - bitwarden_workflow_linter.rules.pinned_job_runner.RuleJobRunnerVersionPinned + - bitwarden_workflow_linter.rules.job_environment_prefix.RuleJobEnvironmentPrefix + - bitwarden_workflow_linter.rules.step_pinned.RuleStepUsesPinned -with open("actions.json", "r", encoding="utf8") as action_file: - approved_actions = json.load(action_file) +approved_actions_path: default_actions.json ``` From a8c70b92f3a7f62dcdd4d8981673389a15f8f389 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 23 Feb 2024 14:48:52 -0800 Subject: [PATCH 47/57] Debugging E2E tests. Need to handle creating the Jobs that don't have Steps (see test_a.yaml) --- lint-workflow-v2/Taskfile.yml | 8 ++++---- lint-workflow-v2/src/bitwarden_workflow_linter/load.py | 3 +++ lint-workflow-v2/tests/fixtures/test-alt.yml | 1 - 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lint-workflow-v2/Taskfile.yml b/lint-workflow-v2/Taskfile.yml index 9e53e9bf..9f8fcafb 100644 --- a/lint-workflow-v2/Taskfile.yml +++ b/lint-workflow-v2/Taskfile.yml @@ -50,19 +50,19 @@ tasks: test:e2e:lint: cmds: - - pipenv run python cli.py -v lint --files tests/fixtures + - pipenv run bwwl lint --files tests/fixtures test:e2e:lint:single: cmds: - - pipenv run python cli.py -v lint --files tests/fixtures/test.yml + - pipenv run bwwl lint --files tests/fixtures/test_a.yml test:e2e:actions:add: cmds: - - pipenv run python cli.py -v actions --output test.json add bitwarden/sm-action + - pipenv run bwwl actions --output test.json add bitwarden/sm-action test:e2e:actions:update: cmds: - - pipenv run python cli.py -v actions update --output test.json + - pipenv run bwwl actions update --output test.json dist: silent: true diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/load.py b/lint-workflow-v2/src/bitwarden_workflow_linter/load.py index bd87a54a..329462a7 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/load.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/load.py @@ -53,6 +53,9 @@ def __build_workflow(cls, loaded_yaml: CommentedMap) -> Workflow: Returns A Workflow to run linting Rules against """ + def _build_job(): + pass + return Workflow.from_dict( { **loaded_yaml, diff --git a/lint-workflow-v2/tests/fixtures/test-alt.yml b/lint-workflow-v2/tests/fixtures/test-alt.yml index 7d01fafe..57822a21 100644 --- a/lint-workflow-v2/tests/fixtures/test-alt.yml +++ b/lint-workflow-v2/tests/fixtures/test-alt.yml @@ -6,7 +6,6 @@ on: inputs: {} jobs: - test-normal-action: name: Download Latest runs-on: ubuntu-20.04 From 17a8fc94b6eed69a812694e9f716201b4708c6c5 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Fri, 23 Feb 2024 14:50:42 -0800 Subject: [PATCH 48/57] Save the latest vim session --- lint-workflow-v2/Session.vim | 149 ++++++++++++++++++++++++++--------- 1 file changed, 112 insertions(+), 37 deletions(-) diff --git a/lint-workflow-v2/Session.vim b/lint-workflow-v2/Session.vim index 9b1879f3..6585ee97 100644 --- a/lint-workflow-v2/Session.vim +++ b/lint-workflow-v2/Session.vim @@ -13,20 +13,30 @@ if &shortmess =~ 'A' else set shortmess=aoO endif -badd +33 tests/test_lint.py -badd +33 tests/test_utils.py -badd +38 Taskfile.yml -badd +0 src/rules/job_environment_prefix.py -badd +0 tests/rules/test_job_environment_prefix.py -badd +0 tests/fixtures/test_a.yaml -badd +4 src/load.py +badd +0 README.md +badd +0 Taskfile.yml +badd +150 src/bitwarden_workflow_linter/utils.py +badd +0 src/bitwarden_workflow_linter/default_actions.json +badd +10 src/bitwarden_workflow_linter/default_settings.py +badd +11 settings.py +badd +5 tests/test_utils.py +badd +2 settings.yaml +badd +0 src/bitwarden_workflow_linter/default_settings.yaml +badd +0 src/bitwarden_workflow_linter/load.py +badd +8 tests/fixtures/test-alt.yml +badd +4 tests/fixtures/test-min.yaml +badd +3 tests/fixtures/test_a.yaml +badd +0 tests/test_load.py +badd +0 src/bitwarden_workflow_linter/models/job.py +badd +0 tests/test_job.py argglobal %argdel -$argadd tests/test_lint.py +$argadd README.md set stal=2 tabnew +setlocal\ bufhidden=wipe +tabnew +setlocal\ bufhidden=wipe tabrewind -edit src/rules/job_environment_prefix.py +edit README.md let s:save_splitbelow = &splitbelow let s:save_splitright = &splitright set splitbelow splitright @@ -46,7 +56,7 @@ set winwidth=1 exe 'vert 1resize ' . ((&columns * 163 + 163) / 327) exe 'vert 2resize ' . ((&columns * 163 + 163) / 327) argglobal -balt tests/rules/test_job_environment_prefix.py +balt Taskfile.yml setlocal fdm=manual setlocal fde=0 setlocal fmr={{{,}}} @@ -57,19 +67,19 @@ setlocal fdn=20 setlocal fen silent! normal! zE let &fdl = &fdl -let s:l = 70 - ((58 * winheight(0) + 31) / 62) +let s:l = 143 - ((61 * winheight(0) + 33) / 67) if s:l < 1 | let s:l = 1 | endif keepjumps exe s:l normal! zt -keepjumps 70 -normal! 018| +keepjumps 143 +normal! 0 wincmd w argglobal -if bufexists(fnamemodify("tests/rules/test_job_environment_prefix.py", ":p")) | buffer tests/rules/test_job_environment_prefix.py | else | edit tests/rules/test_job_environment_prefix.py | endif +if bufexists(fnamemodify("Taskfile.yml", ":p")) | buffer Taskfile.yml | else | edit Taskfile.yml | endif if &buftype ==# 'terminal' - silent file tests/rules/test_job_environment_prefix.py + silent file Taskfile.yml endif -balt src/rules/job_environment_prefix.py +balt README.md setlocal fdm=manual setlocal fde=0 setlocal fmr={{{,}}} @@ -80,17 +90,17 @@ setlocal fdn=20 setlocal fen silent! normal! zE let &fdl = &fdl -let s:l = 40 - ((32 * winheight(0) + 31) / 62) +let s:l = 57 - ((56 * winheight(0) + 33) / 67) if s:l < 1 | let s:l = 1 | endif keepjumps exe s:l normal! zt -keepjumps 40 -normal! 0 +keepjumps 57 +normal! 058| wincmd w exe 'vert 1resize ' . ((&columns * 163 + 163) / 327) exe 'vert 2resize ' . ((&columns * 163 + 163) / 327) tabnext -edit src/load.py +edit src/bitwarden_workflow_linter/load.py let s:save_splitbelow = &splitbelow let s:save_splitright = &splitright set splitbelow splitright @@ -110,10 +120,33 @@ set winminheight=0 set winheight=1 set winminwidth=0 set winwidth=1 -exe 'vert 1resize ' . ((&columns * 109 + 163) / 327) +exe 'vert 1resize ' . ((&columns * 108 + 163) / 327) exe 'vert 2resize ' . ((&columns * 108 + 163) / 327) -exe 'vert 3resize ' . ((&columns * 108 + 163) / 327) +exe 'vert 3resize ' . ((&columns * 109 + 163) / 327) argglobal +balt tests/test_load.py +setlocal fdm=manual +setlocal fde=0 +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=0 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +silent! normal! zE +let &fdl = &fdl +let s:l = 57 - ((32 * winheight(0) + 33) / 67) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 57 +normal! 09| +wincmd w +argglobal +if bufexists(fnamemodify("tests/test_load.py", ":p")) | buffer tests/test_load.py | else | edit tests/test_load.py | endif +if &buftype ==# 'terminal' + silent file tests/test_load.py +endif balt tests/fixtures/test_a.yaml setlocal fdm=manual setlocal fde=0 @@ -125,11 +158,11 @@ setlocal fdn=20 setlocal fen silent! normal! zE let &fdl = &fdl -let s:l = 60 - ((26 * winheight(0) + 30) / 61) +let s:l = 4 - ((3 * winheight(0) + 33) / 67) if s:l < 1 | let s:l = 1 | endif keepjumps exe s:l normal! zt -keepjumps 60 +keepjumps 4 normal! 0 wincmd w argglobal @@ -137,7 +170,7 @@ if bufexists(fnamemodify("tests/fixtures/test_a.yaml", ":p")) | buffer tests/fix if &buftype ==# 'terminal' silent file tests/fixtures/test_a.yaml endif -balt src/load.py +balt tests/test_load.py setlocal fdm=manual setlocal fde=0 setlocal fmr={{{,}}} @@ -148,19 +181,61 @@ setlocal fdn=20 setlocal fen silent! normal! zE let &fdl = &fdl -let s:l = 8 - ((7 * winheight(0) + 30) / 61) +let s:l = 3 - ((2 * winheight(0) + 33) / 67) if s:l < 1 | let s:l = 1 | endif keepjumps exe s:l normal! zt -keepjumps 8 +keepjumps 3 normal! 0 wincmd w +exe 'vert 1resize ' . ((&columns * 108 + 163) / 327) +exe 'vert 2resize ' . ((&columns * 108 + 163) / 327) +exe 'vert 3resize ' . ((&columns * 109 + 163) / 327) +tabnext +edit src/bitwarden_workflow_linter/models/job.py +let s:save_splitbelow = &splitbelow +let s:save_splitright = &splitright +set splitbelow splitright +wincmd _ | wincmd | +vsplit +1wincmd h +wincmd w +let &splitbelow = s:save_splitbelow +let &splitright = s:save_splitright +wincmd t +let s:save_winminheight = &winminheight +let s:save_winminwidth = &winminwidth +set winminheight=0 +set winheight=1 +set winminwidth=0 +set winwidth=1 +exe 'vert 1resize ' . ((&columns * 163 + 163) / 327) +exe 'vert 2resize ' . ((&columns * 163 + 163) / 327) argglobal -if bufexists(fnamemodify("Taskfile.yml", ":p")) | buffer Taskfile.yml | else | edit Taskfile.yml | endif +balt tests/test_job.py +setlocal fdm=manual +setlocal fde=0 +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=0 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +silent! normal! zE +let &fdl = &fdl +let s:l = 1 - ((0 * winheight(0) + 33) / 67) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 1 +normal! 0 +wincmd w +argglobal +if bufexists(fnamemodify("tests/test_job.py", ":p")) | buffer tests/test_job.py | else | edit tests/test_job.py | endif if &buftype ==# 'terminal' - silent file Taskfile.yml + silent file tests/test_job.py endif -balt tests/fixtures/test_a.yaml +balt src/bitwarden_workflow_linter/models/job.py setlocal fdm=manual setlocal fde=0 setlocal fmr={{{,}}} @@ -171,17 +246,17 @@ setlocal fdn=20 setlocal fen silent! normal! zE let &fdl = &fdl -let s:l = 39 - ((38 * winheight(0) + 30) / 61) +let s:l = 11 - ((10 * winheight(0) + 33) / 67) if s:l < 1 | let s:l = 1 | endif keepjumps exe s:l normal! zt -keepjumps 39 -normal! 063| +keepjumps 11 +normal! 0 wincmd w -exe 'vert 1resize ' . ((&columns * 109 + 163) / 327) -exe 'vert 2resize ' . ((&columns * 108 + 163) / 327) -exe 'vert 3resize ' . ((&columns * 108 + 163) / 327) -tabnext 2 +2wincmd w +exe 'vert 1resize ' . ((&columns * 163 + 163) / 327) +exe 'vert 2resize ' . ((&columns * 163 + 163) / 327) +tabnext 3 set stal=1 if exists('s:wipebuf') && len(win_findbuf(s:wipebuf)) == 0 && getbufvar(s:wipebuf, '&buftype') isnot# 'terminal' silent exe 'bwipe ' . s:wipebuf From 14e5ae18f196468de8470282a12695d1c3bd1556 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Wed, 28 Feb 2024 12:52:20 -0800 Subject: [PATCH 49/57] Fixed the complex workflow loading --- .../src/bitwarden_workflow_linter/load.py | 41 ++++--- .../bitwarden_workflow_linter/models/job.py | 29 ++++- .../models/workflow.py | 19 ++- lint-workflow-v2/tests/test_job.py | 73 +++++++++--- lint-workflow-v2/tests/test_load.py | 52 ++++++++- lint-workflow-v2/tests/test_workflow.py | 108 ++++++++++++++---- 6 files changed, 256 insertions(+), 66 deletions(-) diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/load.py b/lint-workflow-v2/src/bitwarden_workflow_linter/load.py index 329462a7..8817f79f 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/load.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/load.py @@ -53,27 +53,26 @@ def __build_workflow(cls, loaded_yaml: CommentedMap) -> Workflow: Returns A Workflow to run linting Rules against """ - def _build_job(): - pass - - return Workflow.from_dict( - { - **loaded_yaml, - "jobs": { - str(job_key): Job.init( - job_key, - { - **job, - "steps": [ - Step.init(idx, job_key, step_data) - for idx, step_data in enumerate(job["steps"]) - ], - }, - ) - for job_key, job in loaded_yaml["jobs"].items() - }, - } - ) + return Workflow.init("", loaded_yaml) + + #return Workflow.from_dict( + # { + # **loaded_yaml, + # "jobs": { + # str(job_key): Job.init( + # job_key, + # { + # **job, + # "steps": [ + # Step.init(idx, job_key, step_data) + # for idx, step_data in enumerate(job["steps"]) + # ], + # }, + # ) + # for job_key, job in loaded_yaml["jobs"].items() + # }, + # } + #) @classmethod def build( diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/models/job.py b/lint-workflow-v2/src/bitwarden_workflow_linter/models/job.py index 861ea451..78e1e35c 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/models/job.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/models/job.py @@ -19,16 +19,39 @@ class Job: be added to this class to make it available for use in linting. """ - runs_on: str = field(metadata=config(field_name="runs-on")) + runs_on: Optional[str] = field(metadata=config(field_name="runs-on"), default=None) key: Optional[str] = None name: Optional[str] = None env: Optional[CommentedMap] = None steps: Optional[List[Step]] = None + uses: Optional[str] = None + uses_path: Optional[str] = None + uses_ref: Optional[str] = None + uses_with: Optional[CommentedMap] = field( + metadata=config(field_name="with"), default=None + ) @classmethod def init(cls: Self, key: str, data: CommentedMap) -> Self: """Custom dataclass constructor to map job data to a Job.""" - new_job = cls.from_dict(data) - new_job.key = key + init_data = { + "key": key, + "name": data["name"] if "name" in data else None, + "runs-on": data["runs-on"] if "runs-on" in data else None, + "env": data["env"] if "env" in data else None + } + + new_job = cls.from_dict(init_data) + + if "steps" in data: + new_job.steps = [ + Step.init(idx, new_job.key, step_data) + for idx, step_data in enumerate(data["steps"]) + ] + else: + new_job.uses = data["uses"].replace("\n", "") + if "@" in new_job.uses: + new_job.uses_path, new_job.uses_ref = new_job.uses.split("@") + return new_job diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/models/workflow.py b/lint-workflow-v2/src/bitwarden_workflow_linter/models/workflow.py index 41b84102..dbcb382f 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/models/workflow.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/models/workflow.py @@ -1,7 +1,7 @@ """Representation for an entire GitHub Action workflow.""" from dataclasses import dataclass -from typing import Dict, Optional +from typing import Dict, Optional, Self from dataclasses_json import dataclass_json, Undefined from ruamel.yaml.comments import CommentedMap @@ -26,3 +26,20 @@ class Workflow: name: Optional[str] = None on: Optional[CommentedMap] = None jobs: Optional[Dict[str, Job]] = None + + @classmethod + def init(cls: Self, key: str, data: CommentedMap) -> Self: + init_data = { + "key": key, + "name": data["name"] if "name" in data else None, + "on": data["on"] if "on" in data else None + } + + new_workflow = cls.from_dict(init_data) + + new_workflow.jobs = { + str(job_key): Job.init(job_key, job) + for job_key, job in data["jobs"].items() + } + + return new_workflow diff --git a/lint-workflow-v2/tests/test_job.py b/lint-workflow-v2/tests/test_job.py index 2a307dc5..0187d9d1 100644 --- a/lint-workflow-v2/tests/test_job.py +++ b/lint-workflow-v2/tests/test_job.py @@ -2,25 +2,61 @@ import pytest +from ruamel.yaml import YAML +from ruamel.yaml.comments import CommentedMap + from src.bitwarden_workflow_linter.models.job import Job from src.bitwarden_workflow_linter.models.step import Step -@pytest.fixture(name="default_job_data") -def fixture_default_job_data(): - return { - "name": "Test", - "runs-on": "ubuntu-latest", - "steps": [Step(run="echo stub")], - } +yaml = YAML() + + +@pytest.fixture(name="workflow_yaml") +def fixture_workflow_yaml(): + return yaml.load( + """\ +--- +name: test +on: + workflow_dispatch: + pull_request: + +jobs: + job-key: + name: Test + runs-on: ubuntu-latest + steps: + - name: Test + run: echo test + + call-workflow: + uses: bitwarden/server/.github/workflows/workflow-linter.yml@master + + test-normal-action: + name: Download Latest + runs-on: ubuntu-20.04 + steps: + - name: Checkout + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + - run: | + echo test -@pytest.fixture(name="default_job") -def fixture_default_job(default_job_data): - return Job.init("default-job", default_job_data) + test-local-action: + name: Testing a local action call + runs-on: ubuntu-20.04 + steps: + - name: local-action + uses: ./version-bump +""" + ) -def test_job_default(default_job): +def test_job_default(workflow_yaml): + default_job_data = workflow_yaml["jobs"]["job-key"] + default_job = Job.init("default-job", default_job_data) + assert default_job.key == "default-job" assert default_job.name == "Test" assert default_job.runs_on == "ubuntu-latest" @@ -28,8 +64,19 @@ def test_job_default(default_job): assert len(default_job.steps) == 1 -def test_job_extra_kwargs(default_job_data): - job = Job.init("test-job", {"extra": "test", **default_job_data}) +def test_uses_job(workflow_yaml): + call_job_data = workflow_yaml["jobs"]["call-workflow"] + call_job = Job.init("call-job", call_job_data) + + assert call_job.key == "call-job" + assert call_job.uses is not None + + +def test_job_extra_kwargs(workflow_yaml): + extra_data_job = workflow_yaml["jobs"]["job-key"] + extra_data_job["extra"] = "This should not exist" + + job = Job.init("job-key", extra_data_job) with pytest.raises(Exception): assert job.extra == "test" diff --git a/lint-workflow-v2/tests/test_load.py b/lint-workflow-v2/tests/test_load.py index beb86c08..9de3b1f7 100644 --- a/lint-workflow-v2/tests/test_load.py +++ b/lint-workflow-v2/tests/test_load.py @@ -19,8 +19,8 @@ def fixture_workflow_filename(): return f"{FIXTURE_DIR}/test.yml" -@pytest.fixture(name="workflow_yaml") -def fixture_workflow_yaml(): +@pytest.fixture(name="simple_workflow_yaml") +def fixture_simple_workflow_yaml(): return yaml.load( """\ --- @@ -38,12 +38,56 @@ def fixture_workflow_yaml(): """ ) +@pytest.fixture(name="complex_workflow_yaml") +def fixture_complex_workflow_yaml(): + return yaml.load( + """\ +--- +name: test +on: + workflow_dispatch: + +jobs: + job-key: + name: Test + runs-on: ubuntu-latest + steps: + - name: Test + run: echo test + + call-workflow: + uses: bitwarden/server/.github/workflows/workflow-linter.yml@master + + test-normal-action: + name: Download Latest + runs-on: ubuntu-20.04 + steps: + - name: Checkout + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + + - run: | + echo test + + test-local-action: + name: Testing a local action call + runs-on: ubuntu-20.04 + steps: + - name: local-action + uses: ./version-bump +""" + ) + def test_load_workflow_from_file(workflow_filename: str) -> None: workflow = WorkflowBuilder.build(workflow_filename) assert isinstance(workflow, Workflow) -def test_load_workflow_from_yaml(workflow_yaml: CommentedMap) -> None: - workflow = WorkflowBuilder.build(workflow=workflow_yaml, from_file=False) +def test_load_simple_workflow_from_yaml(simple_workflow_yaml: CommentedMap) -> None: + workflow = WorkflowBuilder.build(workflow=simple_workflow_yaml, from_file=False) + assert isinstance(workflow, Workflow) + + +def test_load_complex_workflow_from_yaml(complex_workflow_yaml: CommentedMap) -> None: + workflow = WorkflowBuilder.build(workflow=complex_workflow_yaml, from_file=False) assert isinstance(workflow, Workflow) diff --git a/lint-workflow-v2/tests/test_workflow.py b/lint-workflow-v2/tests/test_workflow.py index 9d8d5be0..e862d3c8 100644 --- a/lint-workflow-v2/tests/test_workflow.py +++ b/lint-workflow-v2/tests/test_workflow.py @@ -2,38 +2,98 @@ import pytest +from ruamel.yaml import YAML +from ruamel.yaml.comments import CommentedMap + from src.bitwarden_workflow_linter.models.job import Job from src.bitwarden_workflow_linter.models.step import Step from src.bitwarden_workflow_linter.models.workflow import Workflow -@pytest.fixture(name="default_workflow_data") -def fixture_default_workflow_data(): - return { - "name": "Test Workflow", - "on": {}, - "jobs": { - "job-key": Job.from_dict( - { - "name": "Test", - "runs-on": "ubuntu-latest", - "steps": [Step.from_dict({"run": "echo stub"})], - } - ) - }, - } - - -def test_workflow_default(default_workflow_data): - workflow = Workflow(**default_workflow_data) - - assert workflow.name == "Test Workflow" - assert len(workflow.on.keys()) == 0 +yaml = YAML() + + +@pytest.fixture(name="simple_workflow_yaml") +def fixture_simple_workflow_yaml(): + return yaml.load( + """\ +--- +name: test +on: + workflow_dispatch: + +jobs: + job-key: + name: Test + runs-on: ubuntu-latest + steps: + - name: Test + run: echo test +""" + ) + +@pytest.fixture(name="complex_workflow_yaml") +def fixture_complex_workflow_yaml(): + return yaml.load( + """\ +--- +name: test +on: + workflow_dispatch: + pull_request: + +jobs: + job-key: + name: Test + runs-on: ubuntu-latest + steps: + - name: Test + run: echo test + + call-workflow: + uses: bitwarden/server/.github/workflows/workflow-linter.yml@master + + test-normal-action: + name: Download Latest + runs-on: ubuntu-20.04 + steps: + - name: Checkout + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + + - run: | + echo test + + test-local-action: + name: Testing a local action call + runs-on: ubuntu-20.04 + steps: + - name: local-action + uses: ./version-bump +""" + ) + + +def test_simple_workflow(simple_workflow_yaml): + workflow = Workflow.init("", simple_workflow_yaml) + + assert workflow.name == "test" + assert len(workflow.on.keys()) == 1 assert len(workflow.jobs.keys()) == 1 -def test_workflow_extra_kwargs(default_workflow_data): - workflow = Workflow.from_dict({"extra": "test", **default_workflow_data}) +def test_complex_workflow(complex_workflow_yaml): + workflow = Workflow.init("", complex_workflow_yaml) + + assert workflow.name == "test" + assert len(workflow.on.keys()) == 2 + assert len(workflow.jobs.keys()) == 4 + + +def test_workflow_extra_kwargs(simple_workflow_yaml): + extra_data_workflow = simple_workflow_yaml + extra_data_workflow["extra"] = "This should not exist" + + workflow = Workflow.init("", extra_data_workflow) with pytest.raises(Exception): assert workflow.extra == "test" From 2dc3cdee130cdc2e53593d25ee60511b1e5bb283 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Wed, 28 Feb 2024 12:59:43 -0800 Subject: [PATCH 50/57] Update the JobRunnerVersionPinned rule to support callable workflows --- .../bitwarden_workflow_linter/rules/pinned_job_runner.py | 9 ++++++--- lint-workflow-v2/tests/rules/test_pinned_job_runner.py | 5 +++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/rules/pinned_job_runner.py b/lint-workflow-v2/src/bitwarden_workflow_linter/rules/pinned_job_runner.py index 2124c64a..fab50ce6 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/rules/pinned_job_runner.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/rules/pinned_job_runner.py @@ -44,8 +44,11 @@ def fn(self, obj: Job) -> Tuple[bool, str]: steps: - run: echo test + call-workflow: + uses: bitwarden/server/.github/workflows/workflow-linter.yml@master + 'runs-on' is pinned to '22.04' instead of 'latest' """ - if "latest" not in obj.runs_on: - return True, "" - return False, self.message + if obj.runs_on is not None and "latest" in obj.runs_on: + return False, self.message + return True, "" diff --git a/lint-workflow-v2/tests/rules/test_pinned_job_runner.py b/lint-workflow-v2/tests/rules/test_pinned_job_runner.py index b2d6553f..07a8a208 100644 --- a/lint-workflow-v2/tests/rules/test_pinned_job_runner.py +++ b/lint-workflow-v2/tests/rules/test_pinned_job_runner.py @@ -24,6 +24,9 @@ def fixture_correct_runner(): runs-on: ubuntu-22.04 steps: - run: echo test + + call-workflow: + uses: bitwarden/server/.github/workflows/workflow-linter.yml@master """ return WorkflowBuilder.build(workflow=yaml.load(workflow), from_file=False) @@ -53,6 +56,8 @@ def test_rule_on_correct_runner(rule, correct_runner): result, _ = rule.fn(correct_runner.jobs["job-key"]) assert result is True + result, _ = rule.fn(correct_runner.jobs["call-workflow"]) + assert result is True def test_rule_on_incorrect_runner(rule, incorrect_runner): result, _ = rule.fn(incorrect_runner.jobs["job-key"]) From 92a85c70e6b003056180c308ab216fb89dcc91b3 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Wed, 28 Feb 2024 13:10:38 -0800 Subject: [PATCH 51/57] Fix the lint command to be compatible with callable workflows --- lint-workflow-v2/src/bitwarden_workflow_linter/lint.py | 7 ++++--- lint-workflow-v2/tests/rules/test_pinned_job_runner.py | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/lint.py b/lint-workflow-v2/src/bitwarden_workflow_linter/lint.py index 1a56fd37..242f6c8b 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/lint.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/lint.py @@ -104,9 +104,10 @@ def lint_file(self, filename: str) -> int: for rule in self.rules.job: findings.append(rule.execute(job)) - for step in job.steps: - for rule in self.rules.step: - findings.append(rule.execute(step)) + if job.steps is not None: + for step in job.steps: + for rule in self.rules.step: + findings.append(rule.execute(step)) findings = list(filter(lambda a: a is not None, findings)) diff --git a/lint-workflow-v2/tests/rules/test_pinned_job_runner.py b/lint-workflow-v2/tests/rules/test_pinned_job_runner.py index 07a8a208..9f23024c 100644 --- a/lint-workflow-v2/tests/rules/test_pinned_job_runner.py +++ b/lint-workflow-v2/tests/rules/test_pinned_job_runner.py @@ -59,6 +59,7 @@ def test_rule_on_correct_runner(rule, correct_runner): result, _ = rule.fn(correct_runner.jobs["call-workflow"]) assert result is True + def test_rule_on_incorrect_runner(rule, incorrect_runner): result, _ = rule.fn(incorrect_runner.jobs["job-key"]) assert result is False From 3f231e9e735f46c7a1df7b46358d8fa48e451e18 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Wed, 28 Feb 2024 14:22:07 -0800 Subject: [PATCH 52/57] Format code and fix type annotations --- .../src/bitwarden_workflow_linter/actions.py | 6 ++++-- lint-workflow-v2/src/bitwarden_workflow_linter/cli.py | 4 +++- lint-workflow-v2/src/bitwarden_workflow_linter/load.py | 4 ++-- .../src/bitwarden_workflow_linter/models/job.py | 3 +-- .../src/bitwarden_workflow_linter/models/workflow.py | 2 +- lint-workflow-v2/src/bitwarden_workflow_linter/utils.py | 9 ++++++--- lint-workflow-v2/tests/test_load.py | 1 + lint-workflow-v2/tests/test_workflow.py | 1 + 8 files changed, 19 insertions(+), 11 deletions(-) diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/actions.py b/lint-workflow-v2/src/bitwarden_workflow_linter/actions.py index f62456a0..ebe48c43 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/actions.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/actions.py @@ -158,7 +158,7 @@ def save_actions(self, updated_actions: dict[str, Action], filename: str) -> Non json.dumps(converted_updated_actions, indent=2, sort_keys=True) ) - def add(self, new_action_name: str, filename: str) -> None: + def add(self, new_action_name: str, filename: str) -> int: """Sub-command to add a new Action to the list of approved Actions. 'actions add' will add an Action and all of its metadata and dump all @@ -175,8 +175,9 @@ def add(self, new_action_name: str, filename: str) -> None: updated_actions[latest.name] = latest self.save_actions(updated_actions, filename) + return 0 - def update(self, filename: str) -> None: + def update(self, filename: str) -> int: """Sub-command to update all of the versions of the approved actions. 'actions update' will update all of the approved to the newest version @@ -201,3 +202,4 @@ def update(self, filename: str) -> None: updated_actions[action.name] = latest_release self.save_actions(updated_actions, filename) + return 0 diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/cli.py b/lint-workflow-v2/src/bitwarden_workflow_linter/cli.py index a8c16840..930b0519 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/cli.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/cli.py @@ -3,6 +3,8 @@ import argparse import sys +from typing import List, Optional + from .actions import ActionsCmd from .utils import Settings from .lint import LinterCmd @@ -11,7 +13,7 @@ local_settings = Settings.factory() -def main(input_args: list[str] = None) -> int: +def main(input_args: Optional[List[str]] = None) -> int: """CLI utility to lint GitHub Action Workflows. A CLI utility to enforce coding standards on GitHub Action workflows. The diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/load.py b/lint-workflow-v2/src/bitwarden_workflow_linter/load.py index 8817f79f..4dc52b3b 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/load.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/load.py @@ -55,7 +55,7 @@ def __build_workflow(cls, loaded_yaml: CommentedMap) -> Workflow: """ return Workflow.init("", loaded_yaml) - #return Workflow.from_dict( + # return Workflow.from_dict( # { # **loaded_yaml, # "jobs": { @@ -72,7 +72,7 @@ def __build_workflow(cls, loaded_yaml: CommentedMap) -> Workflow: # for job_key, job in loaded_yaml["jobs"].items() # }, # } - #) + # ) @classmethod def build( diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/models/job.py b/lint-workflow-v2/src/bitwarden_workflow_linter/models/job.py index 78e1e35c..b9ac9081 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/models/job.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/models/job.py @@ -38,7 +38,7 @@ def init(cls: Self, key: str, data: CommentedMap) -> Self: "key": key, "name": data["name"] if "name" in data else None, "runs-on": data["runs-on"] if "runs-on" in data else None, - "env": data["env"] if "env" in data else None + "env": data["env"] if "env" in data else None, } new_job = cls.from_dict(init_data) @@ -53,5 +53,4 @@ def init(cls: Self, key: str, data: CommentedMap) -> Self: if "@" in new_job.uses: new_job.uses_path, new_job.uses_ref = new_job.uses.split("@") - return new_job diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/models/workflow.py b/lint-workflow-v2/src/bitwarden_workflow_linter/models/workflow.py index dbcb382f..761f9f0c 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/models/workflow.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/models/workflow.py @@ -32,7 +32,7 @@ def init(cls: Self, key: str, data: CommentedMap) -> Self: init_data = { "key": key, "name": data["name"] if "name" in data else None, - "on": data["on"] if "on" in data else None + "on": data["on"] if "on" in data else None, } new_workflow = cls.from_dict(init_data) diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/utils.py b/lint-workflow-v2/src/bitwarden_workflow_linter/utils.py index afc4c9e5..4d1cf2ee 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/utils.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/utils.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from enum import Enum -from typing import Optional, Self +from typing import Optional, Self, TypeVar from ruamel.yaml import YAML @@ -107,6 +107,9 @@ class SettingsError(Exception): pass +SettingsFromFactory = TypeVar("SettingsFromFactory", bound="Settings") + + class Settings: """Class that contains configuration-as-code for any portion of the app.""" @@ -117,7 +120,7 @@ def __init__( self, enabled_rules: Optional[list[str]] = None, approved_actions: Optional[dict[str, dict[str, str]]] = None, - ): + ) -> None: """Settings object that can be overriden in settings.py. Args: @@ -140,7 +143,7 @@ def __init__( } @staticmethod - def factory() -> Self: + def factory() -> SettingsFromFactory: with ( importlib.resources.files("bitwarden_workflow_linter") .joinpath("default_settings.yaml") diff --git a/lint-workflow-v2/tests/test_load.py b/lint-workflow-v2/tests/test_load.py index 9de3b1f7..98640af4 100644 --- a/lint-workflow-v2/tests/test_load.py +++ b/lint-workflow-v2/tests/test_load.py @@ -38,6 +38,7 @@ def fixture_simple_workflow_yaml(): """ ) + @pytest.fixture(name="complex_workflow_yaml") def fixture_complex_workflow_yaml(): return yaml.load( diff --git a/lint-workflow-v2/tests/test_workflow.py b/lint-workflow-v2/tests/test_workflow.py index e862d3c8..0c8b2d44 100644 --- a/lint-workflow-v2/tests/test_workflow.py +++ b/lint-workflow-v2/tests/test_workflow.py @@ -32,6 +32,7 @@ def fixture_simple_workflow_yaml(): """ ) + @pytest.fixture(name="complex_workflow_yaml") def fixture_complex_workflow_yaml(): return yaml.load( From 86b9bd9836471a1b684cceff90176048822525d5 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Wed, 28 Feb 2024 14:36:06 -0800 Subject: [PATCH 53/57] Clean up of local opinionated development environment --- lint-workflow-v2/.gitignore | 5 + lint-workflow-v2/Session.vim | 277 ----------------------------------- lint-workflow-v2/flake.lock | 60 -------- lint-workflow-v2/flake.nix | 26 ---- 4 files changed, 5 insertions(+), 363 deletions(-) delete mode 100644 lint-workflow-v2/Session.vim delete mode 100644 lint-workflow-v2/flake.lock delete mode 100644 lint-workflow-v2/flake.nix diff --git a/lint-workflow-v2/.gitignore b/lint-workflow-v2/.gitignore index 40575ed1..1e69ed6a 100644 --- a/lint-workflow-v2/.gitignore +++ b/lint-workflow-v2/.gitignore @@ -1,2 +1,7 @@ .coverage dist + +## Dev Environments +Session.vim +flake.nix +flake.lock diff --git a/lint-workflow-v2/Session.vim b/lint-workflow-v2/Session.vim deleted file mode 100644 index 6585ee97..00000000 --- a/lint-workflow-v2/Session.vim +++ /dev/null @@ -1,277 +0,0 @@ -let SessionLoad = 1 -let s:so_save = &g:so | let s:siso_save = &g:siso | setg so=0 siso=0 | setl so=-1 siso=-1 -let v:this_session=expand(":p") -silent only -silent tabonly -cd ~/bitwarden/official-repos/gh-actions/lint-workflow-v2 -if expand('%') == '' && !&modified && line('$') <= 1 && getline(1) == '' - let s:wipebuf = bufnr('%') -endif -let s:shortmess_save = &shortmess -if &shortmess =~ 'A' - set shortmess=aoOA -else - set shortmess=aoO -endif -badd +0 README.md -badd +0 Taskfile.yml -badd +150 src/bitwarden_workflow_linter/utils.py -badd +0 src/bitwarden_workflow_linter/default_actions.json -badd +10 src/bitwarden_workflow_linter/default_settings.py -badd +11 settings.py -badd +5 tests/test_utils.py -badd +2 settings.yaml -badd +0 src/bitwarden_workflow_linter/default_settings.yaml -badd +0 src/bitwarden_workflow_linter/load.py -badd +8 tests/fixtures/test-alt.yml -badd +4 tests/fixtures/test-min.yaml -badd +3 tests/fixtures/test_a.yaml -badd +0 tests/test_load.py -badd +0 src/bitwarden_workflow_linter/models/job.py -badd +0 tests/test_job.py -argglobal -%argdel -$argadd README.md -set stal=2 -tabnew +setlocal\ bufhidden=wipe -tabnew +setlocal\ bufhidden=wipe -tabrewind -edit README.md -let s:save_splitbelow = &splitbelow -let s:save_splitright = &splitright -set splitbelow splitright -wincmd _ | wincmd | -vsplit -1wincmd h -wincmd w -let &splitbelow = s:save_splitbelow -let &splitright = s:save_splitright -wincmd t -let s:save_winminheight = &winminheight -let s:save_winminwidth = &winminwidth -set winminheight=0 -set winheight=1 -set winminwidth=0 -set winwidth=1 -exe 'vert 1resize ' . ((&columns * 163 + 163) / 327) -exe 'vert 2resize ' . ((&columns * 163 + 163) / 327) -argglobal -balt Taskfile.yml -setlocal fdm=manual -setlocal fde=0 -setlocal fmr={{{,}}} -setlocal fdi=# -setlocal fdl=0 -setlocal fml=1 -setlocal fdn=20 -setlocal fen -silent! normal! zE -let &fdl = &fdl -let s:l = 143 - ((61 * winheight(0) + 33) / 67) -if s:l < 1 | let s:l = 1 | endif -keepjumps exe s:l -normal! zt -keepjumps 143 -normal! 0 -wincmd w -argglobal -if bufexists(fnamemodify("Taskfile.yml", ":p")) | buffer Taskfile.yml | else | edit Taskfile.yml | endif -if &buftype ==# 'terminal' - silent file Taskfile.yml -endif -balt README.md -setlocal fdm=manual -setlocal fde=0 -setlocal fmr={{{,}}} -setlocal fdi=# -setlocal fdl=0 -setlocal fml=1 -setlocal fdn=20 -setlocal fen -silent! normal! zE -let &fdl = &fdl -let s:l = 57 - ((56 * winheight(0) + 33) / 67) -if s:l < 1 | let s:l = 1 | endif -keepjumps exe s:l -normal! zt -keepjumps 57 -normal! 058| -wincmd w -exe 'vert 1resize ' . ((&columns * 163 + 163) / 327) -exe 'vert 2resize ' . ((&columns * 163 + 163) / 327) -tabnext -edit src/bitwarden_workflow_linter/load.py -let s:save_splitbelow = &splitbelow -let s:save_splitright = &splitright -set splitbelow splitright -wincmd _ | wincmd | -vsplit -wincmd _ | wincmd | -vsplit -2wincmd h -wincmd w -wincmd w -let &splitbelow = s:save_splitbelow -let &splitright = s:save_splitright -wincmd t -let s:save_winminheight = &winminheight -let s:save_winminwidth = &winminwidth -set winminheight=0 -set winheight=1 -set winminwidth=0 -set winwidth=1 -exe 'vert 1resize ' . ((&columns * 108 + 163) / 327) -exe 'vert 2resize ' . ((&columns * 108 + 163) / 327) -exe 'vert 3resize ' . ((&columns * 109 + 163) / 327) -argglobal -balt tests/test_load.py -setlocal fdm=manual -setlocal fde=0 -setlocal fmr={{{,}}} -setlocal fdi=# -setlocal fdl=0 -setlocal fml=1 -setlocal fdn=20 -setlocal fen -silent! normal! zE -let &fdl = &fdl -let s:l = 57 - ((32 * winheight(0) + 33) / 67) -if s:l < 1 | let s:l = 1 | endif -keepjumps exe s:l -normal! zt -keepjumps 57 -normal! 09| -wincmd w -argglobal -if bufexists(fnamemodify("tests/test_load.py", ":p")) | buffer tests/test_load.py | else | edit tests/test_load.py | endif -if &buftype ==# 'terminal' - silent file tests/test_load.py -endif -balt tests/fixtures/test_a.yaml -setlocal fdm=manual -setlocal fde=0 -setlocal fmr={{{,}}} -setlocal fdi=# -setlocal fdl=0 -setlocal fml=1 -setlocal fdn=20 -setlocal fen -silent! normal! zE -let &fdl = &fdl -let s:l = 4 - ((3 * winheight(0) + 33) / 67) -if s:l < 1 | let s:l = 1 | endif -keepjumps exe s:l -normal! zt -keepjumps 4 -normal! 0 -wincmd w -argglobal -if bufexists(fnamemodify("tests/fixtures/test_a.yaml", ":p")) | buffer tests/fixtures/test_a.yaml | else | edit tests/fixtures/test_a.yaml | endif -if &buftype ==# 'terminal' - silent file tests/fixtures/test_a.yaml -endif -balt tests/test_load.py -setlocal fdm=manual -setlocal fde=0 -setlocal fmr={{{,}}} -setlocal fdi=# -setlocal fdl=0 -setlocal fml=1 -setlocal fdn=20 -setlocal fen -silent! normal! zE -let &fdl = &fdl -let s:l = 3 - ((2 * winheight(0) + 33) / 67) -if s:l < 1 | let s:l = 1 | endif -keepjumps exe s:l -normal! zt -keepjumps 3 -normal! 0 -wincmd w -exe 'vert 1resize ' . ((&columns * 108 + 163) / 327) -exe 'vert 2resize ' . ((&columns * 108 + 163) / 327) -exe 'vert 3resize ' . ((&columns * 109 + 163) / 327) -tabnext -edit src/bitwarden_workflow_linter/models/job.py -let s:save_splitbelow = &splitbelow -let s:save_splitright = &splitright -set splitbelow splitright -wincmd _ | wincmd | -vsplit -1wincmd h -wincmd w -let &splitbelow = s:save_splitbelow -let &splitright = s:save_splitright -wincmd t -let s:save_winminheight = &winminheight -let s:save_winminwidth = &winminwidth -set winminheight=0 -set winheight=1 -set winminwidth=0 -set winwidth=1 -exe 'vert 1resize ' . ((&columns * 163 + 163) / 327) -exe 'vert 2resize ' . ((&columns * 163 + 163) / 327) -argglobal -balt tests/test_job.py -setlocal fdm=manual -setlocal fde=0 -setlocal fmr={{{,}}} -setlocal fdi=# -setlocal fdl=0 -setlocal fml=1 -setlocal fdn=20 -setlocal fen -silent! normal! zE -let &fdl = &fdl -let s:l = 1 - ((0 * winheight(0) + 33) / 67) -if s:l < 1 | let s:l = 1 | endif -keepjumps exe s:l -normal! zt -keepjumps 1 -normal! 0 -wincmd w -argglobal -if bufexists(fnamemodify("tests/test_job.py", ":p")) | buffer tests/test_job.py | else | edit tests/test_job.py | endif -if &buftype ==# 'terminal' - silent file tests/test_job.py -endif -balt src/bitwarden_workflow_linter/models/job.py -setlocal fdm=manual -setlocal fde=0 -setlocal fmr={{{,}}} -setlocal fdi=# -setlocal fdl=0 -setlocal fml=1 -setlocal fdn=20 -setlocal fen -silent! normal! zE -let &fdl = &fdl -let s:l = 11 - ((10 * winheight(0) + 33) / 67) -if s:l < 1 | let s:l = 1 | endif -keepjumps exe s:l -normal! zt -keepjumps 11 -normal! 0 -wincmd w -2wincmd w -exe 'vert 1resize ' . ((&columns * 163 + 163) / 327) -exe 'vert 2resize ' . ((&columns * 163 + 163) / 327) -tabnext 3 -set stal=1 -if exists('s:wipebuf') && len(win_findbuf(s:wipebuf)) == 0 && getbufvar(s:wipebuf, '&buftype') isnot# 'terminal' - silent exe 'bwipe ' . s:wipebuf -endif -unlet! s:wipebuf -set winheight=1 winwidth=20 -let &shortmess = s:shortmess_save -let &winminheight = s:save_winminheight -let &winminwidth = s:save_winminwidth -let s:sx = expand(":p:r")."x.vim" -if filereadable(s:sx) - exe "source " . fnameescape(s:sx) -endif -let &g:so = s:so_save | let &g:siso = s:siso_save -set hlsearch -doautoall SessionLoadPost -unlet SessionLoad -" vim: set ft=vim : diff --git a/lint-workflow-v2/flake.lock b/lint-workflow-v2/flake.lock deleted file mode 100644 index 27363ca4..00000000 --- a/lint-workflow-v2/flake.lock +++ /dev/null @@ -1,60 +0,0 @@ -{ - "nodes": { - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1705309234, - "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", - "type": "github" - }, - "original": { - "id": "flake-utils", - "type": "indirect" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1706683685, - "narHash": "sha256-FtPPshEpxH/ewBOsdKBNhlsL2MLEFv1hEnQ19f/bFsQ=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "5ad9903c16126a7d949101687af0aa589b1d7d3d", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/lint-workflow-v2/flake.nix b/lint-workflow-v2/flake.nix deleted file mode 100644 index 885768e5..00000000 --- a/lint-workflow-v2/flake.nix +++ /dev/null @@ -1,26 +0,0 @@ -{ - description = "GitHub Action Linter"; - inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; }; - - outputs = { self, nixpkgs, flake-utils}: - flake-utils.lib.eachDefaultSystem (system: - let - #pkgs = nixpkgs.legacyPackages.x86_64-linux.pkgs; - pkgs = nixpkgs.legacyPackages.${system}; - packageName = "workflow-linter"; - in { - devShells.default = pkgs.mkShell { - name = "${packageName}"; - buildInputs = [ - pkgs.envsubst - pkgs.pipenv - pkgs.python311 - ]; - shellHook = '' - echo "Welcome in $name" - export PS1="\[\e[1;33m\][nix(workflow-linter)]\$\[\e[0m\] " - ''; - }; - } - ); -} From ed12be655d8d430a342ee294eafb0b068aaf204a Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Wed, 28 Feb 2024 14:44:24 -0800 Subject: [PATCH 54/57] Moving actions sub command to be in beta since we haven't decided to adopt that pattern --- lint-workflow-v2/src/bitwarden_workflow_linter/actions.py | 2 +- lint-workflow-v2/src/bitwarden_workflow_linter/cli.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/actions.py b/lint-workflow-v2/src/bitwarden_workflow_linter/actions.py index ebe48c43..c8744150 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/actions.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/actions.py @@ -49,7 +49,7 @@ def extend_parser( The main argument parser to add sub commands and arguments to """ parser_actions = subparsers.add_parser( - "actions", help="Add or Update Actions in the pre-approved list." + "actions", help="!!BETA!!\nAdd or Update Actions in the pre-approved list." ) parser_actions.add_argument( "-o", "--output", action="store", default="actions.json" diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/cli.py b/lint-workflow-v2/src/bitwarden_workflow_linter/cli.py index 930b0519..13e984a7 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/cli.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/cli.py @@ -42,6 +42,7 @@ def main(input_args: Optional[List[str]] = None) -> int: return linter_cmd.run(args.files, args.strict) if args.command == "actions": + print(f"{'-'*50}\n!!bwwl actions is in BETA!!\n{'-'*50}") if args.actions_command == "add": return actions_cmd.add(args.name, args.output) if args.actions_command == "update": From 1c0c8f31c81a3c3580a1bba9a5abec232fdf8cf1 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Thu, 29 Feb 2024 11:19:31 -0800 Subject: [PATCH 55/57] Update all spelling errors identified and path issues (Thanks @vgrassia!) --- lint-workflow-v2/README.md | 4 +-- lint-workflow-v2/pyproject.toml | 3 +-- lint-workflow-v2/pyproject.toml.tpl | 3 +-- .../src/bitwarden_workflow_linter/actions.py | 24 ++++++++--------- .../src/bitwarden_workflow_linter/cli.py | 4 +-- .../src/bitwarden_workflow_linter/lint.py | 14 +++++----- .../src/bitwarden_workflow_linter/load.py | 25 +++--------------- .../bitwarden_workflow_linter/models/job.py | 2 +- .../bitwarden_workflow_linter/models/step.py | 2 +- .../models/workflow.py | 2 +- .../src/bitwarden_workflow_linter/rule.py | 6 ++--- .../rules/job_environment_prefix.py | 10 +++---- .../rules/name_capitalized.py | 4 +-- .../rules/name_exists.py | 6 ++--- .../rules/pinned_job_runner.py | 6 ++--- .../rules/step_approved.py | 6 ++--- .../rules/step_pinned.py | 4 +-- .../src/bitwarden_workflow_linter/utils.py | 6 ++--- lint-workflow-v2/tests/conftest.py | 2 +- lint-workflow-v2/tests/fixtures/test.yml | 26 +++++++------------ .../rules/test_job_environment_prefix.py | 2 +- .../tests/rules/test_name_capitalized.py | 2 +- .../tests/rules/test_name_exists.py | 2 +- .../tests/rules/test_pinned_job_runner.py | 2 +- .../tests/rules/test_step_approved.py | 2 +- .../tests/rules/test_step_pinned.py | 2 +- lint-workflow-v2/tests/test_job.py | 2 +- lint-workflow-v2/tests/test_lint.py | 2 +- lint-workflow-v2/tests/test_load.py | 2 +- lint-workflow-v2/tests/test_rule.py | 2 +- lint-workflow-v2/tests/test_step.py | 2 +- lint-workflow-v2/tests/test_utils.py | 2 +- lint-workflow-v2/tests/test_workflow.py | 2 +- 33 files changed, 78 insertions(+), 107 deletions(-) diff --git a/lint-workflow-v2/README.md b/lint-workflow-v2/README.md index d9f4cc1e..557ca6d1 100644 --- a/lint-workflow-v2/README.md +++ b/lint-workflow-v2/README.md @@ -61,7 +61,7 @@ pipenv shell ### Testing -All built-in `src/rules` should have 100% code coverage and we should shoot for an overall coverage of 80%+. +All built-in `src/bitwarden_workflow_linter/rules` should have 100% code coverage and we should shoot for an overall coverage of 80%+. We are lax on the [imperative shell](https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell) (code interacting with other systems; ie. disk, network, etc), but we strive to maintain a high coverage over the @@ -124,8 +124,6 @@ class RuleJobNameExists(Rule): return False, self.message ``` -[TODO: Is this enough documentation on how to use?] - By default, a new Rule needs five things: - `self.message`: The message to return to the user on a lint failure diff --git a/lint-workflow-v2/pyproject.toml b/lint-workflow-v2/pyproject.toml index 7f695bdd..cf65d04f 100644 --- a/lint-workflow-v2/pyproject.toml +++ b/lint-workflow-v2/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "bitwarden_workflow_linter" dynamic = ["version"] -authors = [] +authors = ["Bitwarden Inc"] description = "Custom GitHub Action Workflow Linter" readme = "README.md" requires-python = ">=3.11" @@ -35,7 +35,6 @@ dependencies = [ Homepage = "https://github.com/bitwarden/gh-actions/tree/main/lint-workflow-v2" Issues = "https://github.com/bitwarden/gh-actions/issues" - [project.scripts] bwwl = "bitwarden_workflow_linter.cli:main" diff --git a/lint-workflow-v2/pyproject.toml.tpl b/lint-workflow-v2/pyproject.toml.tpl index cb3a2601..472b8471 100644 --- a/lint-workflow-v2/pyproject.toml.tpl +++ b/lint-workflow-v2/pyproject.toml.tpl @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "bitwarden_workflow_linter" dynamic = ["version"] -authors = [] +authors = ["Bitwarden Inc"] description = "Custom GitHub Action Workflow Linter" readme = "README.md" requires-python = ">=3.11" @@ -23,7 +23,6 @@ $DEPS Homepage = "https://github.com/bitwarden/gh-actions/tree/main/lint-workflow-v2" Issues = "https://github.com/bitwarden/gh-actions/issues" - [project.scripts] bwwl = "bitwarden_workflow_linter.cli:main" diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/actions.py b/lint-workflow-v2/src/bitwarden_workflow_linter/actions.py index c8744150..90cd0bae 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/actions.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/actions.py @@ -26,11 +26,11 @@ class ActionsCmd: """ def __init__(self, settings: Optional[Settings] = None) -> None: - """Initialize the the ActionsCmd class. + """Initialize the ActionsCmd class. Args: settings: - A Settings object that contains any default, overriden, or custom settings + A Settings object that contains any default, overridden, or custom settings required anywhere in the application. """ self.settings = settings @@ -41,12 +41,12 @@ def extend_parser( ) -> argparse._SubParsersAction: """Extends the CLI subparser with the options for ActionCmd. - Add 'actions add' and 'actions update' to the CLI as sub-commands + Add 'actions add' and 'actions update' to the CLI as subcommands along with the options and arguments for each. Args: subparsers: - The main argument parser to add sub commands and arguments to + The main argument parser to add subcommands and arguments to """ parser_actions = subparsers.add_parser( "actions", help="!!BETA!!\nAdd or Update Actions in the pre-approved list." @@ -61,7 +61,7 @@ def extend_parser( parser_actions_add = subparsers_actions.add_parser( "add", help="add action to approved list" ) - parser_actions_add.add_argument("name", help="action name [git owener/repo]") + parser_actions_add.add_argument("name", help="action name [git owner/repo]") return subparsers @@ -96,13 +96,13 @@ def get_github_api_response( return response def exists(self, action: Action) -> bool: - """Takes and action id and checks if the action repo exists.""" + """Takes an action id and checks if the action repository exists.""" url = f"https://api.github.com/repos/{action.name}" response = self.get_github_api_response(url, action.name) if response is None: - # Handle github api limit exceed by returning that the action exists + # Handle exceeding GitHub API limit by returning that the action exists # without actually checking to prevent false errors on linter output. Only # show it as an linter error. return True @@ -159,7 +159,7 @@ def save_actions(self, updated_actions: dict[str, Action], filename: str) -> Non ) def add(self, new_action_name: str, filename: str) -> int: - """Sub-command to add a new Action to the list of approved Actions. + """Subcommand to add a new Action to the list of approved Actions. 'actions add' will add an Action and all of its metadata and dump all approved actions (including the new one) to either the default JSON file @@ -178,11 +178,11 @@ def add(self, new_action_name: str, filename: str) -> int: return 0 def update(self, filename: str) -> int: - """Sub-command to update all of the versions of the approved actions. + """Subcommand to update all of the versions of the approved actions. - 'actions update' will update all of the approved to the newest version - and dump all of the new data to either the default JSON file or the - one provided by '--output' + 'actions update' will update all of the approved actions to the newest + version and dump all of the new data to either the default JSON file or + the one provided by '--output' """ print("Actions: update") updated_actions = {} diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/cli.py b/lint-workflow-v2/src/bitwarden_workflow_linter/cli.py index 13e984a7..6f9e662a 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/cli.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/cli.py @@ -6,8 +6,8 @@ from typing import List, Optional from .actions import ActionsCmd -from .utils import Settings from .lint import LinterCmd +from .utils import Settings local_settings = Settings.factory() @@ -17,7 +17,7 @@ def main(input_args: Optional[List[str]] = None) -> int: """CLI utility to lint GitHub Action Workflows. A CLI utility to enforce coding standards on GitHub Action workflows. The - utility also provides other sub-commands to assist with other workflow + utility also provides other subcommands to assist with other workflow maintenance tasks; such as maintaining the list of approved GitHub Actions. """ linter_cmd = LinterCmd(settings=local_settings) diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/lint.py b/lint-workflow-v2/src/bitwarden_workflow_linter/lint.py index 242f6c8b..ebc18165 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/lint.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/lint.py @@ -21,11 +21,11 @@ class LinterCmd: """ def __init__(self, settings: Optional[Settings] = None) -> None: - """Initailized the LinterCmd class. + """Initailize the LinterCmd class. Args: settings: - A Settings object that contains any default, overriden, or custom settings + A Settings object that contains any default, overridden, or custom settings required anywhere in the application. """ self.rules = Rules(settings=settings) @@ -36,11 +36,11 @@ def extend_parser( ) -> argparse._SubParsersAction: """Extends the CLI subparser with the options for LintCmd. - Add 'lint' as a sub command along with its options and arguments + Add 'lint' as a subcommand along with its options and arguments Args: subparsers: - The main argument parser to add sub commands and arguments to + The main argument parser to add subcommands and arguments to """ parser_lint = subparsers.add_parser( "lint", @@ -69,7 +69,7 @@ def get_max_error_level(self, findings: list[LintFinding]) -> int: Args: findings: - All of the findings that the linter found while linting a workflows. + All of the findings that the linter found while linting a workflow. Return: The numeric value of the maximum lint finding @@ -127,7 +127,7 @@ def generate_files(self, files: list[str]) -> list[str]: Args: files: - list of file names or director names. + list of file names or directory names. Returns: A sorted set of all workflow files in the path(s) specified. @@ -150,7 +150,7 @@ def run(self, input_files: list[str], strict: bool = False) -> int: Args: input_files: - list of file names or director names. + list of file names or directory names. strict: fail on WARNING instead of succeed diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/load.py b/lint-workflow-v2/src/bitwarden_workflow_linter/load.py index 4dc52b3b..a378f0f6 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/load.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/load.py @@ -1,4 +1,4 @@ -"""Module to load for Worflows and Rules.""" +"""Module to load for Workflows and Rules.""" import importlib @@ -36,7 +36,7 @@ def __load_workflow_from_file(cls, filename: str) -> CommentedMap: Returns: A CommentedMap that contains the dict() representation of the - yaml file. It includes the comments as a part of their respective + YAML file. It includes the comments as a part of their respective objects (depending on their location in the file). """ with open(filename, encoding="utf8") as file: @@ -55,25 +55,6 @@ def __build_workflow(cls, loaded_yaml: CommentedMap) -> Workflow: """ return Workflow.init("", loaded_yaml) - # return Workflow.from_dict( - # { - # **loaded_yaml, - # "jobs": { - # str(job_key): Job.init( - # job_key, - # { - # **job, - # "steps": [ - # Step.init(idx, job_key, step_data) - # for idx, step_data in enumerate(job["steps"]) - # ], - # }, - # ) - # for job_key, job in loaded_yaml["jobs"].items() - # }, - # } - # ) - @classmethod def build( cls, @@ -128,7 +109,7 @@ def __init__(self, settings: Settings) -> None: Args: settings: - A Settings object that contains any default, overriden, or custom settings + A Settings object that contains any default, overridden, or custom settings required anywhere in the application. """ for rule in settings.enabled_rules: diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/models/job.py b/lint-workflow-v2/src/bitwarden_workflow_linter/models/job.py index b9ac9081..8915ba33 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/models/job.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/models/job.py @@ -15,7 +15,7 @@ class Job: """Represents a job in a GitHub Action workflow. This object contains all of the data that is required to run the current linting - Rules against. If a new Rule requies a key that is missing, the attribute should + Rules against. If a new Rule requires a key that is missing, the attribute should be added to this class to make it available for use in linting. """ diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/models/step.py b/lint-workflow-v2/src/bitwarden_workflow_linter/models/step.py index 34012fa0..e18f89d3 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/models/step.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/models/step.py @@ -13,7 +13,7 @@ class Step: """Represents a step in a GitHub Action workflow job. This object contains all of the data that is required to run the current linting - Rules against. If a new Rule requies a key that is missing, the attribute should + Rules against. If a new Rule requires a key that is missing, the attribute should be added to this class to make it available for use in linting. """ diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/models/workflow.py b/lint-workflow-v2/src/bitwarden_workflow_linter/models/workflow.py index 761f9f0c..9c909695 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/models/workflow.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/models/workflow.py @@ -15,7 +15,7 @@ class Workflow: """Represents an entire workflow in a GitHub Action workflow. This object contains all of the data that is required to run the current linting - Rules against. If a new Rule requies a key that is missing, the attribute should + Rules against. If a new Rule requires a key that is missing, the attribute should be added to this class to make it available for use in linting. See src/models/job.py for an example if the key in the workflow data does not map diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/rule.py b/lint-workflow-v2/src/bitwarden_workflow_linter/rule.py index 3149cf69..c48a8b6d 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/rule.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/rule.py @@ -9,7 +9,7 @@ class RuleExecutionException(Exception): - """Exeception for the Base Rule class.""" + """Exception for the Base Rule class.""" pass @@ -22,7 +22,7 @@ class Rule: settings: Optional[Settings] = None def fn(self, obj: Union[Workflow, Job, Step]) -> Tuple[bool, str]: - """Execute the Rule (this should be overriden in the extending class. + """Execute the Rule (this should be overridden in the extending class. Args: obj: @@ -58,7 +58,7 @@ def build_lint_message(self, message: str, obj: Union[Workflow, Job, Step]) -> s return f"{obj_type.__name__} => {message}" def execute(self, obj: Union[Workflow, Job, Step]) -> Union[LintFinding, None]: - """Wrapper function to execute the overriden self.fn(). + """Wrapper function to execute the overridden self.fn(). Run the Rule against the object and return the results. The result could be an Exception message where the Rule cannot be run against diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/rules/job_environment_prefix.py b/lint-workflow-v2/src/bitwarden_workflow_linter/rules/job_environment_prefix.py index 94040871..ec031752 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/rules/job_environment_prefix.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/rules/job_environment_prefix.py @@ -3,14 +3,14 @@ from typing import Union, Optional, Tuple, List from ..models.job import Job -from ..models.workflow import Workflow from ..models.step import Step +from ..models.workflow import Workflow from ..rule import Rule from ..utils import LintLevels, Settings class RuleJobEnvironmentPrefix(Rule): - """Rule to enforce specific prefixes for environemnt variables. + """Rule to enforce specific prefixes for environment variables. Automated testing is not easily written for GitHub Action Workflows. CI can also get complicated really quickly and take up hundreds of lines. All of this can @@ -29,10 +29,10 @@ def __init__(self, settings: Optional[Settings] = None) -> None: Args: settings: - A Settings object that contains any default, overriden, or custom settings + A Settings object that contains any default, overridden, or custom settings required anywhere in the application. """ - self.message = "Job environment vars should start with and underscore:" + self.message = "Job environment vars should start with an underscore:" self.on_fail = LintLevels.ERROR self.compatibility = [Job] self.settings = settings @@ -57,7 +57,7 @@ def fn(self, obj: Job) -> Tuple[bool, str]: as in _TEST_ENV. See tests/rules/test_job_environment_prefix.py for examples of - incorrectly names environment variables. + incorrectly named environment variables. """ correct = True diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/rules/name_capitalized.py b/lint-workflow-v2/src/bitwarden_workflow_linter/rules/name_capitalized.py index 71cb2598..ac205542 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/rules/name_capitalized.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/rules/name_capitalized.py @@ -2,9 +2,9 @@ from typing import Optional, Tuple, Union -from ..models.workflow import Workflow from ..models.job import Job from ..models.step import Step +from ..models.workflow import Workflow from ..rule import Rule from ..utils import LintLevels, Settings @@ -20,7 +20,7 @@ def __init__(self, settings: Optional[Settings] = None) -> None: Args: settings: - A Settings object that contains any default, overriden, or custom settings + A Settings object that contains any default, overridden, or custom settings required anywhere in the application. """ self.message = "name must capitalized" diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/rules/name_exists.py b/lint-workflow-v2/src/bitwarden_workflow_linter/rules/name_exists.py index 124214a9..ef261bfa 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/rules/name_exists.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/rules/name_exists.py @@ -1,4 +1,4 @@ -"""A Rule to enforce that a 'name' exists.""" +"""A Rule to enforce that a 'name' key exists.""" from typing import Optional, Tuple, Union @@ -10,7 +10,7 @@ class RuleNameExists(Rule): - """Rule to enforce a 'name' exists for every object in GitHub Actions. + """Rule to enforce a 'name' key exists for every object in GitHub Actions. For pipeline run troubleshooting and debugging, it is helpful to have a name to immediately identify a Workflow, Job, or Step while moving between @@ -24,7 +24,7 @@ def __init__(self, settings: Optional[Settings] = None) -> None: Args: settings: - A Settings object that contains any default, overriden, or custom settings + A Settings object that contains any default, overridden, or custom settings required anywhere in the application. """ self.message = "name must exist" diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/rules/pinned_job_runner.py b/lint-workflow-v2/src/bitwarden_workflow_linter/rules/pinned_job_runner.py index fab50ce6..2301e991 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/rules/pinned_job_runner.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/rules/pinned_job_runner.py @@ -3,8 +3,8 @@ from typing import List, Optional, Tuple, Union from ..models.job import Job -from ..models.workflow import Workflow from ..models.step import Step +from ..models.workflow import Workflow from ..rule import Rule from ..utils import LintLevels, Settings @@ -12,7 +12,7 @@ class RuleJobRunnerVersionPinned(Rule): """Rule to enforce pinned Runner OS versions. - `*-latest` versions updating without knowing has broken all of our worklfows + Using `*-latest` versions will update automatically and broken all of our workflows in the past. To avoid this and prevent a single event from breaking the majority of our pipelines, we pin the versions. """ @@ -22,7 +22,7 @@ def __init__(self, settings: Optional[Settings] = None) -> None: Args: settings: - A Settings object that contains any default, overriden, or custom settings + A Settings object that contains any default, overridden, or custom settings required anywhere in the application. """ self.message = "Workflow runner must be pinned" diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/rules/step_approved.py b/lint-workflow-v2/src/bitwarden_workflow_linter/rules/step_approved.py index 0b820438..ac6d831a 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/rules/step_approved.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/rules/step_approved.py @@ -3,8 +3,8 @@ from typing import List, Optional, Tuple, Union from ..models.job import Job -from ..models.workflow import Workflow from ..models.step import Step +from ..models.workflow import Workflow from ..rule import Rule from ..utils import LintLevels, Settings @@ -22,7 +22,7 @@ def __init__(self, settings: Optional[Settings] = None) -> None: Args: settings: - A Settings object that contains any default, overriden, or custom settings + A Settings object that contains any default, overridden, or custom settings required anywhere in the application. """ self.on_fail = LintLevels.WARNING @@ -88,7 +88,7 @@ def fn(self, obj: Step) -> Tuple[bool, str]: if obj.uses and not obj.uses_path in self.settings.approved_actions: return False, ( f"New Action detected: {obj.uses_path}\nFor security purposes, " - "actions must be reviewed and on the pre-approved list" + "actions must be reviewed and be on the pre-approved list" ) action = self.settings.approved_actions[obj.uses_path] diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/rules/step_pinned.py b/lint-workflow-v2/src/bitwarden_workflow_linter/rules/step_pinned.py index c9b2bcac..295a3f70 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/rules/step_pinned.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/rules/step_pinned.py @@ -20,7 +20,7 @@ class RuleStepUsesPinned(Rule): pipelines, we pin any use of an Action to a specific hash that has been verified and pre-approved after a security audit of the version of the Action. - All Internl Actions, should be pinned to 'main'. This prevents Renovate from + All Internal Actions, should be pinned to 'main'. This prevents Renovate from spamming a bunch of PRs across all of our repos when `bitwarden/gh-actions` is updated. """ @@ -30,7 +30,7 @@ def __init__(self, settings: Optional[Settings] = None) -> None: Args: settings: - A Settings object that contains any default, overriden, or custom settings + A Settings object that contains any default, overridden, or custom settings required anywhere in the application. """ self.on_fail = LintLevels.ERROR diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/utils.py b/lint-workflow-v2/src/bitwarden_workflow_linter/utils.py index 4d1cf2ee..c0e3c704 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/utils.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/utils.py @@ -80,7 +80,7 @@ def __eq__(self, other: Self) -> bool: Another Action type object to compare Return - The state of eqaulity + The state of equality """ return ( self.name == other.name @@ -96,7 +96,7 @@ def __ne__(self, other: Self) -> bool: Another Action type object to compare Return - The negation of the state of eqaulity + The negation of the state of equality """ return not self.__eq__(other) @@ -121,7 +121,7 @@ def __init__( enabled_rules: Optional[list[str]] = None, approved_actions: Optional[dict[str, dict[str, str]]] = None, ) -> None: - """Settings object that can be overriden in settings.py. + """Settings object that can be overridden in settings.py. Args: enabled_rules: diff --git a/lint-workflow-v2/tests/conftest.py b/lint-workflow-v2/tests/conftest.py index 89ebcc46..af48cf42 100644 --- a/lint-workflow-v2/tests/conftest.py +++ b/lint-workflow-v2/tests/conftest.py @@ -1,3 +1,3 @@ -"""Shared configation for tests.""" +"""Shared configuration for tests.""" FIXTURE_DIR = "./tests/fixtures" diff --git a/lint-workflow-v2/tests/fixtures/test.yml b/lint-workflow-v2/tests/fixtures/test.yml index cd6b71a2..ca1af310 100644 --- a/lint-workflow-v2/tests/fixtures/test.yml +++ b/lint-workflow-v2/tests/fixtures/test.yml @@ -17,24 +17,18 @@ jobs: - name: Checkout repo uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.3.4 - - name: Login to Azure - uses: Azure/logi@77f1b2e3fb80c0e8645114159d17008b8a2e475a + + - name: Log in to Azure - CI subscription + uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7 with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - - name: Retrieve secrets - id: retrieve-secrets - env: - KEYVAULT: bitwarden-prod-kv - SECRETS: | - crowdin-api-token - run: | - for i in ${SECRETS//,/ } - do - VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $i --query value --output tsv) - echo "::add-mask::$VALUE" - echo "$i=$VALUE" >> $GITHUB_OUTPUT - done + - name: Retrieve GitHub PAT secrets + id: retrieve-secret-pat + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: "bitwarden-ci" + secrets: "crowdin-api-token" - uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea # v1.3.2 env: diff --git a/lint-workflow-v2/tests/rules/test_job_environment_prefix.py b/lint-workflow-v2/tests/rules/test_job_environment_prefix.py index 4c3afbef..89ae7d5c 100644 --- a/lint-workflow-v2/tests/rules/test_job_environment_prefix.py +++ b/lint-workflow-v2/tests/rules/test_job_environment_prefix.py @@ -1,4 +1,4 @@ -"""Test src/rules/job_environment_prefix.""" +"""Test src/bitwarden_workflow_linter/rules/job_environment_prefix.""" import pytest diff --git a/lint-workflow-v2/tests/rules/test_name_capitalized.py b/lint-workflow-v2/tests/rules/test_name_capitalized.py index 6317987f..1e573b5b 100644 --- a/lint-workflow-v2/tests/rules/test_name_capitalized.py +++ b/lint-workflow-v2/tests/rules/test_name_capitalized.py @@ -1,4 +1,4 @@ -"""Test src/rules/name_capitalized.py.""" +"""Test src/bitwarden_workflow_linter/rules/name_capitalized.py.""" import pytest diff --git a/lint-workflow-v2/tests/rules/test_name_exists.py b/lint-workflow-v2/tests/rules/test_name_exists.py index 935acc79..02048dad 100644 --- a/lint-workflow-v2/tests/rules/test_name_exists.py +++ b/lint-workflow-v2/tests/rules/test_name_exists.py @@ -1,4 +1,4 @@ -"""Test src/rules/name_exists.py.""" +"""Test src/bitwarden_workflow_linter/rules/name_exists.py.""" import pytest diff --git a/lint-workflow-v2/tests/rules/test_pinned_job_runner.py b/lint-workflow-v2/tests/rules/test_pinned_job_runner.py index 9f23024c..5db43d0a 100644 --- a/lint-workflow-v2/tests/rules/test_pinned_job_runner.py +++ b/lint-workflow-v2/tests/rules/test_pinned_job_runner.py @@ -1,4 +1,4 @@ -"""Test src/rules/pinned_job_runner.py.""" +"""Test src/bitwarden_workflow_linter/rules/pinned_job_runner.py.""" import pytest diff --git a/lint-workflow-v2/tests/rules/test_step_approved.py b/lint-workflow-v2/tests/rules/test_step_approved.py index efa082e4..5f1f0942 100644 --- a/lint-workflow-v2/tests/rules/test_step_approved.py +++ b/lint-workflow-v2/tests/rules/test_step_approved.py @@ -1,4 +1,4 @@ -"""Test src/rules/step_approved.py.""" +"""Test src/bitwarden_workflow_linter/rules/step_approved.py.""" import pytest diff --git a/lint-workflow-v2/tests/rules/test_step_pinned.py b/lint-workflow-v2/tests/rules/test_step_pinned.py index cb5d7f06..b73e0c58 100644 --- a/lint-workflow-v2/tests/rules/test_step_pinned.py +++ b/lint-workflow-v2/tests/rules/test_step_pinned.py @@ -1,4 +1,4 @@ -"""Test src/rules/step_pinned.py.""" +"""Test src/bitwarden_workflow_linter/rules/step_pinned.py.""" import pytest diff --git a/lint-workflow-v2/tests/test_job.py b/lint-workflow-v2/tests/test_job.py index 0187d9d1..f0a91105 100644 --- a/lint-workflow-v2/tests/test_job.py +++ b/lint-workflow-v2/tests/test_job.py @@ -1,4 +1,4 @@ -"""Test src/models/job.py.""" +"""Test src/bitwarden_workflow_linter/models/job.py.""" import pytest diff --git a/lint-workflow-v2/tests/test_lint.py b/lint-workflow-v2/tests/test_lint.py index fad17139..68ce399c 100644 --- a/lint-workflow-v2/tests/test_lint.py +++ b/lint-workflow-v2/tests/test_lint.py @@ -1,4 +1,4 @@ -"""Test src/lint.py.""" +"""Test src/bitwarden_workflow_linter/lint.py.""" import pytest diff --git a/lint-workflow-v2/tests/test_load.py b/lint-workflow-v2/tests/test_load.py index 98640af4..cedc2017 100644 --- a/lint-workflow-v2/tests/test_load.py +++ b/lint-workflow-v2/tests/test_load.py @@ -1,4 +1,4 @@ -"""Tests src/load.py.""" +"""Tests src/bitwarden_workflow_linter/load.py.""" import pytest diff --git a/lint-workflow-v2/tests/test_rule.py b/lint-workflow-v2/tests/test_rule.py index 363e2eb8..44dba8c5 100644 --- a/lint-workflow-v2/tests/test_rule.py +++ b/lint-workflow-v2/tests/test_rule.py @@ -1,4 +1,4 @@ -"""Tests src/rule.py.""" +"""Tests src/bitwarden_workflow_linter/rule.py.""" import pytest from typing import Union diff --git a/lint-workflow-v2/tests/test_step.py b/lint-workflow-v2/tests/test_step.py index deda0f9a..23625d16 100644 --- a/lint-workflow-v2/tests/test_step.py +++ b/lint-workflow-v2/tests/test_step.py @@ -1,4 +1,4 @@ -"""Test src/models/step.py.""" +"""Test src/bitwarden_workflow_linter/models/step.py.""" import json import pytest diff --git a/lint-workflow-v2/tests/test_utils.py b/lint-workflow-v2/tests/test_utils.py index b80b9efa..baf75d2d 100644 --- a/lint-workflow-v2/tests/test_utils.py +++ b/lint-workflow-v2/tests/test_utils.py @@ -1,4 +1,4 @@ -"""Tests src/utils.py.""" +"""Tests src/bitwarden_workflow_linter/utils.py.""" from src.bitwarden_workflow_linter.utils import Action, Colors, LintFinding, LintLevels diff --git a/lint-workflow-v2/tests/test_workflow.py b/lint-workflow-v2/tests/test_workflow.py index 0c8b2d44..da35f1f2 100644 --- a/lint-workflow-v2/tests/test_workflow.py +++ b/lint-workflow-v2/tests/test_workflow.py @@ -1,4 +1,4 @@ -"""Test src/models/workflow.py.""" +"""Test src/bitwarden_workflow_linter/models/workflow.py.""" import pytest From e1951c752bf4cf264a899f4af7c6f484b10e7510 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Thu, 29 Feb 2024 12:04:51 -0800 Subject: [PATCH 56/57] Add hacky safety measure to alert on GitHub Response Schema changes --- lint-workflow-v2/Taskfile.yml | 2 +- .../src/bitwarden_workflow_linter/actions.py | 56 +++++++++++-------- .../src/bitwarden_workflow_linter/load.py | 1 + 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/lint-workflow-v2/Taskfile.yml b/lint-workflow-v2/Taskfile.yml index 9f8fcafb..00cc69c4 100644 --- a/lint-workflow-v2/Taskfile.yml +++ b/lint-workflow-v2/Taskfile.yml @@ -62,7 +62,7 @@ tasks: test:e2e:actions:update: cmds: - - pipenv run bwwl actions update --output test.json + - pipenv run bwwl actions --output test.json update dist: silent: true diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/actions.py b/lint-workflow-v2/src/bitwarden_workflow_linter/actions.py index 90cd0bae..88698127 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/actions.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/actions.py @@ -12,6 +12,12 @@ from .utils import Colors, Settings, Action +class GitHubApiSchemaError(Exception): + """A generic Exception to catch redefinitions of GitHub Api Schema changes.""" + + pass + + class ActionsCmd: """Command to manage the pre-approved list of Actions @@ -115,33 +121,39 @@ def exists(self, action: Action) -> bool: def get_latest_version(self, action: Action) -> Action | None: """Gets the latest version of the Action to compare against.""" - # Get tag from latest release - response = self.get_github_api_response( - f"https://api.github.com/repos/{action.name}/releases/latest", action.name - ) - if not response: - return None - - tag_name = json.loads(response.data)["tag_name"] + try: + # Get tag from latest release + response = self.get_github_api_response( + f"https://api.github.com/repos/{action.name}/releases/latest", + action.name, + ) + if not response: + return None - # Get the URL to the commit for the tag - response = self.get_github_api_response( - f"https://api.github.com/repos/{action.name}/git/ref/tags/{tag_name}", - action.name, - ) - if not response: - return None + tag_name = json.loads(response.data)["tag_name"] - if json.loads(response.data)["object"]["type"] == "commit": - sha = json.loads(response.data)["object"]["sha"] - else: - url = json.loads(response.data)["object"]["url"] - # Follow the URL and get the commit sha for tags - response = self.get_github_api_response(url, action.name) + # Get the URL to the commit for the tag + response = self.get_github_api_response( + f"https://api.github.com/repos/{action.name}/git/ref/tags/{tag_name}", + action.name, + ) if not response: return None - sha = json.loads(response.data)["object"]["sha"] + if json.loads(response.data)["object"]["type"] == "commit": + sha = json.loads(response.data)["object"]["sha"] + else: + url = json.loads(response.data)["object"]["url"] + # Follow the URL and get the commit sha for tags + response = self.get_github_api_response(url, action.name) + if not response: + return None + + sha = json.loads(response.data)["object"]["sha"] + except KeyError as err: + raise GitHubApiSchemaError( + f"Error with the GitHub API Response Schema for either /releases or /tags: {err}" + ) return Action(name=action.name, version=tag_name, sha=sha) diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/load.py b/lint-workflow-v2/src/bitwarden_workflow_linter/load.py index a378f0f6..7f863725 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/load.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/load.py @@ -112,6 +112,7 @@ def __init__(self, settings: Settings) -> None: A Settings object that contains any default, overridden, or custom settings required anywhere in the application. """ + # [TODO]: data resiliency for rule in settings.enabled_rules: module_name = rule.split(".") module_name = ".".join(module_name[:-1]) From de9be6f041753b65494f945c6fc931ec73f38a01 Mon Sep 17 00:00:00 2001 From: Joseph Flinn Date: Wed, 6 Mar 2024 14:01:29 -0800 Subject: [PATCH 57/57] Fixed spelling issues --- lint-workflow-v2/README.md | 4 ++-- .../src/bitwarden_workflow_linter/rules/name_capitalized.py | 2 +- .../src/bitwarden_workflow_linter/rules/name_exists.py | 2 +- .../bitwarden_workflow_linter/rules/pinned_job_runner.py | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lint-workflow-v2/README.md b/lint-workflow-v2/README.md index 557ca6d1..12dac3e1 100644 --- a/lint-workflow-v2/README.md +++ b/lint-workflow-v2/README.md @@ -65,7 +65,7 @@ All built-in `src/bitwarden_workflow_linter/rules` should have 100% code coverag We are lax on the [imperative shell](https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell) (code interacting with other systems; ie. disk, network, etc), but we strive to maintain a high coverage over the -funcationl core (objects and models). +functional core (objects and models). ``` pipenv shell @@ -133,7 +133,7 @@ By default, a new Rule needs five things: - `self.compatibility`: The list of objects this rule is compatible with. This is used to create separate instances of the Rule for each object in the Rules collection. - `self.settings`: In general, this should default to what is shown here, but allows for overrides -- `self.fn`: The function doing the actual work to check the object and enforce the standardenforcing. +- `self.fn`: The function doing the actual work to check the object and enforce the standard. `fn` can be as simple or as complex as it needs to be to run a check on a _single_ object. This linter currently does not support Rules that check against multiple objects at a time OR file level formatting (one empty between each step or diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/rules/name_capitalized.py b/lint-workflow-v2/src/bitwarden_workflow_linter/rules/name_capitalized.py index ac205542..d27dc98c 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/rules/name_capitalized.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/rules/name_capitalized.py @@ -16,7 +16,7 @@ class RuleNameCapitalized(Rule): """ def __init__(self, settings: Optional[Settings] = None) -> None: - """Contructor for RuleNameCapitalized to override the Rule class. + """Constructor for RuleNameCapitalized to override the Rule class. Args: settings: diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/rules/name_exists.py b/lint-workflow-v2/src/bitwarden_workflow_linter/rules/name_exists.py index ef261bfa..94fc9237 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/rules/name_exists.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/rules/name_exists.py @@ -20,7 +20,7 @@ class RuleNameExists(Rule): """ def __init__(self, settings: Optional[Settings] = None) -> None: - """Contructor for RuleNameCapitalized to override Rule class. + """Constructor for RuleNameCapitalized to override Rule class. Args: settings: diff --git a/lint-workflow-v2/src/bitwarden_workflow_linter/rules/pinned_job_runner.py b/lint-workflow-v2/src/bitwarden_workflow_linter/rules/pinned_job_runner.py index 2301e991..b84d2c92 100644 --- a/lint-workflow-v2/src/bitwarden_workflow_linter/rules/pinned_job_runner.py +++ b/lint-workflow-v2/src/bitwarden_workflow_linter/rules/pinned_job_runner.py @@ -12,9 +12,9 @@ class RuleJobRunnerVersionPinned(Rule): """Rule to enforce pinned Runner OS versions. - Using `*-latest` versions will update automatically and broken all of our workflows - in the past. To avoid this and prevent a single event from breaking the majority of - our pipelines, we pin the versions. + Using `*-latest` versions will update automatically and has broken all of + our workflows in the past. To avoid this and prevent a single event from + breaking the majority of our pipelines, we pin the versions. """ def __init__(self, settings: Optional[Settings] = None) -> None: