diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 7267a65..469e9c9 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -12,22 +12,17 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- neovim_version: ['nightly']
+ neovim_version: ['stable', 'nightly']
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- run: date +%F > todays-date
- name: Restore cache for today's nightly.
- uses: actions/cache@v2
+ uses: actions/cache@v4
with:
path: _neovim
key: ${{ runner.os }}-x64-${{ hashFiles('todays-date') }}
- - name: Prepare plenary
- run: |
- git clone --depth 1 https://github.com/nvim-lua/plenary.nvim ~/.local/share/nvim/site/pack/vendor/start/plenary.nvim
- ln -s "$(pwd)" ~/.local/share/nvim/site/pack/vendor/start
-
- name: Build rust module
run: |
sudo apt install -y libluajit-5.1-dev
@@ -40,5 +35,4 @@ jobs:
version: ${{ matrix.neovim_version }}
- name: Run tests
- run: |
- nvim --headless --noplugin -u tests/minimal.vim -c "PlenaryBustedDirectory tests/ {minimal_init = 'tests/minimal.vim'}"
+ run: make test
diff --git a/.gitignore b/.gitignore
index 2cf7b49..c1be013 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,6 @@
-lua/deps
-lua/libgh_actions_rust.so
+lua/pipeline_native/deps
+lua/pipeline_native/yaml.so
target
+
+.tests
diff --git a/.luacheckrc b/.luacheckrc
index 490f98f..9d9b0f2 100644
--- a/.luacheckrc
+++ b/.luacheckrc
@@ -1,5 +1,6 @@
ignore = {
- "631", -- max_line_length
+ '212', -- unused_argument
+ '631', -- max_line_length
}
read_globals = {
vim = {
@@ -15,7 +16,7 @@ read_globals = {
},
},
},
- "describe",
- "it",
- "assert",
+ 'describe',
+ 'it',
+ 'assert',
}
diff --git a/Cargo.lock b/Cargo.lock
index 5077b4f..a4cab52 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,276 +3,284 @@
version = 3
[[package]]
-name = "aho-corasick"
-version = "0.7.20"
+name = "autocfg"
+version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
-dependencies = [
- "memchr",
-]
+checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
-name = "autocfg"
-version = "1.1.0"
+name = "bitflags"
+version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "bstr"
-version = "0.2.17"
+version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
+checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22"
dependencies = [
"memchr",
+ "serde",
]
[[package]]
name = "cc"
-version = "1.0.79"
+version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
-
-[[package]]
-name = "either"
-version = "1.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
-
-[[package]]
-name = "erased-serde"
-version = "0.3.25"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f2b0c2380453a92ea8b6c8e5f64ecaafccddde8ceab55ff7a8ac1029f894569"
+checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47"
dependencies = [
- "serde",
+ "shlex",
]
[[package]]
-name = "futures-core"
-version = "0.3.28"
+name = "cfg-if"
+version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
-name = "futures-macro"
-version = "0.3.28"
+name = "either"
+version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.15",
-]
+checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
-name = "futures-task"
-version = "0.3.28"
+name = "equivalent"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
-name = "futures-util"
-version = "0.3.28"
+name = "erased-serde"
+version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
-dependencies = [
- "futures-core",
- "futures-macro",
- "futures-task",
- "pin-project-lite",
- "pin-utils",
- "slab",
-]
-
-[[package]]
-name = "gh-actions-rust"
-version = "0.0.1"
+checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d"
dependencies = [
- "mlua",
"serde",
- "serde_yaml",
+ "typeid",
]
[[package]]
name = "hashbrown"
-version = "0.12.3"
+version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3"
[[package]]
name = "indexmap"
-version = "1.9.3"
+version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
dependencies = [
- "autocfg",
+ "equivalent",
"hashbrown",
]
[[package]]
-name = "itertools"
-version = "0.10.5"
+name = "itoa"
+version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
-dependencies = [
- "either",
-]
+checksum = "7a73e9fe3c49d7afb2ace819fa181a287ce54a0983eda4e0eb05c22f82ffe534"
[[package]]
-name = "itoa"
-version = "1.0.6"
+name = "libc"
+version = "0.2.164"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
+checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f"
+
+[[package]]
+name = "lock_api"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
[[package]]
name = "memchr"
-version = "2.5.0"
+version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "mlua"
-version = "0.6.6"
+version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4235d7e740d73d7429df6f176c81b248f05c39d67264d45a7d8cecb67c227f6f"
+checksum = "0ae9546e4a268c309804e8bbb7526e31cbfdedca7cd60ac1b987d0b212e0d876"
dependencies = [
"bstr",
- "cc",
+ "either",
"erased-serde",
- "futures-core",
- "futures-task",
- "futures-util",
+ "mlua-sys",
"mlua_derive",
"num-traits",
- "once_cell",
- "pkg-config",
+ "parking_lot",
+ "rustc-hash",
"serde",
+ "serde-value",
+]
+
+[[package]]
+name = "mlua-sys"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "efa6bf1a64f06848749b7e7727417f4ec2121599e2a10ef0a8a3888b0e9a5a0d"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "pkg-config",
]
[[package]]
name = "mlua_derive"
-version = "0.6.0"
+version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1713774a29db53a48932596dc943439dd54eb56a9efaace716719cc10fa82d5b"
+checksum = "2cfc5faa2e0d044b3f5f0879be2920e0a711c97744c42cf1c295cb183668933e"
dependencies = [
- "itertools",
- "once_cell",
- "proc-macro-error",
"proc-macro2",
"quote",
- "regex",
- "syn 1.0.109",
+ "syn",
]
[[package]]
name = "num-traits"
-version = "0.2.15"
+version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
-name = "once_cell"
-version = "1.17.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
-
-[[package]]
-name = "pin-project-lite"
-version = "0.2.9"
+name = "ordered-float"
+version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
+checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c"
+dependencies = [
+ "num-traits",
+]
[[package]]
-name = "pin-utils"
-version = "0.1.0"
+name = "parking_lot"
+version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
[[package]]
-name = "pkg-config"
-version = "0.3.26"
+name = "parking_lot_core"
+version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
+checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets",
+]
[[package]]
-name = "proc-macro-error"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+name = "pipeline_native"
+version = "0.0.1"
dependencies = [
- "proc-macro-error-attr",
- "proc-macro2",
- "quote",
- "syn 1.0.109",
- "version_check",
+ "mlua",
+ "serde",
+ "serde_yaml",
]
[[package]]
-name = "proc-macro-error-attr"
-version = "1.0.4"
+name = "pkg-config"
+version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
-dependencies = [
- "proc-macro2",
- "quote",
- "version_check",
-]
+checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "proc-macro2"
-version = "1.0.56"
+version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435"
+checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
-version = "1.0.26"
+version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
+checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
-name = "regex"
-version = "1.7.3"
+name = "redox_syscall"
+version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d"
+checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f"
dependencies = [
- "aho-corasick",
- "memchr",
- "regex-syntax",
+ "bitflags",
]
[[package]]
-name = "regex-syntax"
-version = "0.6.29"
+name = "rustc-hash"
+version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
+checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152"
[[package]]
name = "ryu"
-version = "1.0.13"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
-version = "1.0.160"
+version = "1.0.215"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c"
+checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-value"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
+dependencies = [
+ "ordered-float",
+ "serde",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.215"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
[[package]]
name = "serde_yaml"
-version = "0.9.21"
+version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9d684e3ec7de3bf5466b32bd75303ac16f0736426e5a4e0d6e489559ce1249c"
+checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap",
"itoa",
@@ -282,41 +290,39 @@ dependencies = [
]
[[package]]
-name = "slab"
-version = "0.4.8"
+name = "shlex"
+version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d"
-dependencies = [
- "autocfg",
-]
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
-name = "syn"
-version = "1.0.109"
+name = "smallvec"
+version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
+checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "syn"
-version = "2.0.15"
+version = "2.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822"
+checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
+[[package]]
+name = "typeid"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e"
+
[[package]]
name = "unicode-ident"
-version = "1.0.8"
+version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
+checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
[[package]]
name = "unsafe-libyaml"
@@ -325,7 +331,65 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
-name = "version_check"
-version = "0.9.4"
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
diff --git a/Cargo.toml b/Cargo.toml
index 0e4712b..fc14f74 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,5 +1,5 @@
[package]
-name = 'gh-actions-rust'
+name = 'pipeline_native'
version = '0.0.1'
edition = '2021'
@@ -7,6 +7,6 @@ edition = '2021'
crate-type = ["cdylib"]
[dependencies]
-mlua = {version = "0.6", features = ["luajit", "module", "macros", "async", "serialize"]}
+mlua = { version = "0.10", features = ["luajit", "module", "serialize"] }
serde = "1.0"
serde_yaml = "0.9"
diff --git a/Makefile b/Makefile
index 212fef8..7dd872d 100644
--- a/Makefile
+++ b/Makefile
@@ -1,9 +1,35 @@
-.PHONY: build
+.PHONY: all build clean prepare_test test
+
+all: build copy
+
+clean:
+ rm -rf ./lua/pipeline_native/yaml.so ./lua/pipeline_native/deps
+
+depsdir:
+ mkdir -p ./lua/pipeline_native/deps
build:
cargo build --release
- mkdir -p ./lua/deps/
- rm -f ./lua/libgh_actions_rust.so
- cp ./target/release/libgh_actions_rust.dylib ./lua/libgh_actions_rust.so || true
- cp ./target/release/libgh_actions_rust.so ./lua/libgh_actions_rust.so || true
- cp ./target/release/deps/*.rlib ./lua/deps/
+
+copy: clean depsdir
+ cp ./target/release/libpipeline_rust.dylib ./lua/pipeline_native/yaml.so || true
+ cp ./target/release/libpipeline_rust.so ./lua/pipeline_native/yaml.so || true
+ cp ./target/release/deps/*.rlib ./lua/pipeline_native/deps/
+
+plugin_dir := ./.tests/site/pack/deps/start
+plugins := $(plugin_dir)/plenary.nvim $(plugin_dir)/nui.nvim
+
+$(plugin_dir)/plenary.nvim:
+ git clone --depth 1 https://github.com/nvim-lua/plenary.nvim $(plugin_dir)/plenary.nvim
+
+$(plugin_dir)/nui.nvim:
+ git clone --depth 1 https://github.com/MunifTanjim/nui.nvim $(plugin_dir)/nui.nvim
+
+prepare_test:
+ mkdir -p $(plugin_dir)
+
+test: prepare_test $(plugins)
+ nvim --headless --noplugin -u tests/init.lua -c "PlenaryBustedDirectory tests/ {minimal_init = 'tests/init.lua'}"
+
+clean-test:
+ rm -rf ./.tests
diff --git a/README.md b/README.md
index cf44507..e7648e8 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,27 @@
-# gh-actions.nvim
+# pipeline.nvim
-The gh-actions plugin for Neovim allows developers to easily manage and dispatch their GitHub Actions workflow runs directly from within the editor.
+The pipeline.nvim plugin for Neovim allows developers to easily manage and dispatch their CI/CD Pipelines, like GitHub Actions or Gitlab CI, directly from within the editor.
-
+
+## CI/CD Platform Support
+
+- [GitHub Actions](https://github.com/features/actions)
+- [Gitlab CI/CD](https://docs.gitlab.com/ee/ci/) (fairly untested, feel free to
+ report bugs or open PRs)
+
## Features
-- List workflows and their runs for the current repository
-- Run/dispatch workflows with `workflow_dispatch`
+- List pipelines and their runs for the current repository
+- Run/dispatch pipelines with `workflow_dispatch`
## ToDo
-- Rerun a failed workflow
+- Rerun a failed pipeline or job
- Configurable keybindings
-- `gw` (goto workflow) should open workflow file in buffer instead of browser
-- Allow to cycle between inputs on workflow_dispatch
+- Allow to cycle between inputs on dispatch
## Installation
@@ -27,62 +32,72 @@ Either have the cli [yq](https://github.com/mikefarah/yq) installed or:
- [GNU Make](https://www.gnu.org/software/make/)
- [Cargo](https://doc.rust-lang.org/cargo/)
+Additionally, the Gitlab provider needs the [`glab`](https://docs.gitlab.com/ee/editor_extensions/gitlab_cli/) cli to be installed.
+
### lazy.nvim
Using [lazy.nvim](https://github.com/folke/lazy.nvim)
```lua
{
- 'topaxi/gh-actions.nvim',
+ 'topaxi/pipeline.nvim',
keys = {
- { 'gh', 'GhActions', desc = 'Open Github Actions' },
+ { 'ci', 'Pipeline', desc = 'Open pipeline.nvim' },
},
-- optional, you can also install and use `yq` instead.
build = 'make',
- ---@type GhActionsConfig
+ ---@type pipeline.Config
opts = {},
},
```
## Authentication
+### GitHub
+
The plugin requires authentication with your GitHub account to access your workflows and runs. You can authenticate by running the `gh auth login command` in your terminal and following the prompts.
Alternatively, define a `GITHUB_TOKEN` variable in your environment.
+### Gitlab
+
+The plugin interacts with Gitlab via the `glab` cli, all that is needed is being authenticated through `glab auth login`.
+
## Usage
### Commands
-- `:GhActions` or `:GhActions toggle` toggles the `gh-actions` split
-- `:GhActions open` opens the `gh-actions` split
-- `:GhActions close` closes the `gh-actions` split
+- `:Pipeline` or `:Pipeline toggle` toggles the `pipeline.nvim` split
+- `:Pipeline open` opens the `pipeline.nvim` split
+- `:Pipeline close` closes the `pipeline.nvim` split
### Keybindings
The following keybindings are provided by the plugin:
-- `q` - closes the `gh-actions` the split
-- `gw` - open the workflow file below the cursor on GitHub
-- `gr` - open the workflow run below the cursor on GitHub
+- `q` - closes the `pipeline.nvim` the split
+- `gp` - open the pipeline below the cursor on GitHub
+- `gr` - open the run below the cursor on GitHub
- `gj` - open the job of the workflow run below the cursor on GitHub
- `d` - dispatch a new run for the workflow below the cursor on GitHub
### Options
-The default options (as defined in [lua/config.lua](./blob/main/lua/gh-actions/config.lua))
+The default options (as defined in [lua/config.lua](./blob/main/lua/pipeline/config.lua))
```lua
{
--- The browser executable path to open workflow runs/jobs in
- ---@type string|nil
browser = nil,
--- Interval to refresh in seconds
refresh_interval = 10,
--- How much workflow runs and jobs should be indented
indent = 2,
+ providers = {
+ github = {},
+ gitlab = {},
+ },
--- Allowed hosts to fetch data from, github.com is always allowed
- --- @type string[]
allowed_hosts = {},
icons = {
workflow_dispatch = '⚡️',
@@ -103,21 +118,21 @@ The default options (as defined in [lua/config.lua](./blob/main/lua/gh-actions/c
},
},
highlights = {
- GhActionsRunIconSuccess = { link = 'LspDiagnosticsVirtualTextHint' },
- GhActionsRunIconFailure = { link = 'LspDiagnosticsVirtualTextError' },
- GhActionsRunIconStartup_failure = { link = 'LspDiagnosticsVirtualTextError' },
- GhActionsRunIconPending = { link = 'LspDiagnosticsVirtualTextWarning' },
- GhActionsRunIconRequested = { link = 'LspDiagnosticsVirtualTextWarning' },
- GhActionsRunIconWaiting = { link = 'LspDiagnosticsVirtualTextWarning' },
- GhActionsRunIconIn_progress = { link = 'LspDiagnosticsVirtualTextWarning' },
- GhActionsRunIconCancelled = { link = 'Comment' },
- GhActionsRunIconSkipped = { link = 'Comment' },
- GhActionsRunCancelled = { link = 'Comment' },
- GhActionsRunSkipped = { link = 'Comment' },
- GhActionsJobCancelled = { link = 'Comment' },
- GhActionsJobSkipped = { link = 'Comment' },
- GhActionsStepCancelled = { link = 'Comment' },
- GhActionsStepSkipped = { link = 'Comment' },
+ PipelineRunIconSuccess = { link = 'LspDiagnosticsVirtualTextHint' },
+ PipelineRunIconFailure = { link = 'LspDiagnosticsVirtualTextError' },
+ PipelineRunIconStartup_failure = { link = 'LspDiagnosticsVirtualTextError' },
+ PipelineRunIconPending = { link = 'LspDiagnosticsVirtualTextWarning' },
+ PipelineRunIconRequested = { link = 'LspDiagnosticsVirtualTextWarning' },
+ PipelineRunIconWaiting = { link = 'LspDiagnosticsVirtualTextWarning' },
+ PipelineRunIconIn_progress = { link = 'LspDiagnosticsVirtualTextWarning' },
+ PipelineRunIconCancelled = { link = 'Comment' },
+ PipelineRunIconSkipped = { link = 'Comment' },
+ PipelineRunCancelled = { link = 'Comment' },
+ PipelineRunSkipped = { link = 'Comment' },
+ PipelineJobCancelled = { link = 'Comment' },
+ PipelineJobSkipped = { link = 'Comment' },
+ PipelineStepCancelled = { link = 'Comment' },
+ PipelineStepSkipped = { link = 'Comment' },
},
split = {
relative = 'editor',
@@ -142,7 +157,7 @@ The default options (as defined in [lua/config.lua](./blob/main/lua/gh-actions/c
require('lualine').setup({
sections = {
lualine_a = {
- { 'gh-actions' },
+ { 'pipeline' },
},
}
})
@@ -155,7 +170,7 @@ require('lualine').setup({
sections = {
lualine_a = {
-- with default options
- { 'gh-actions', icon = '' },
+ { 'pipeline', icon = '' },
},
}
})
diff --git a/lazy.lua b/lazy.lua
index c9e575b..d76df74 100644
--- a/lazy.lua
+++ b/lazy.lua
@@ -1,11 +1,14 @@
return {
{ 'nvim-lua/plenary.nvim', lazy = true },
- { 'MunifTanjim/nui.nvim', lazy = true },
+ { 'MunifTanjim/nui.nvim', lazy = true },
{
- 'topaxi/gh-actions.nvim',
- cmd = 'GhActions',
+ 'topaxi/pipeline.nvim',
+ cmd = { 'Pipeline', 'GhActions' },
lazy = true,
dependencies = { 'nvim-lua/plenary.nvim', 'MunifTanjim/nui.nvim' },
opts = {},
+ config = function(_, opts)
+ require('pipeline').setup(opts)
+ end,
},
}
diff --git a/lua/gh-actions/command.lua b/lua/gh-actions/command.lua
deleted file mode 100644
index 9d182ac..0000000
--- a/lua/gh-actions/command.lua
+++ /dev/null
@@ -1,38 +0,0 @@
-local M = {}
-
-local function handle_gh_actions_command(a)
- local gha = require('gh-actions')
-
- local action = a.fargs[1] or 'toggle'
-
- if action == 'open' then
- return gha.open()
- elseif action == 'close' then
- return gha.close()
- elseif action == 'toggle' then
- local ui = require('gh-actions.ui')
-
- if ui.split.winid then
- return gha.close()
- else
- return gha.open()
- end
- end
-end
-
-local function completion_customlist()
- return {
- 'open',
- 'close',
- 'toggle',
- }
-end
-
-function M.setup()
- vim.api.nvim_create_user_command('GhActions', handle_gh_actions_command, {
- nargs = '?',
- complete = completion_customlist,
- })
-end
-
-return M
diff --git a/lua/gh-actions/config.lua b/lua/gh-actions/config.lua
deleted file mode 100644
index f88a8ff..0000000
--- a/lua/gh-actions/config.lua
+++ /dev/null
@@ -1,83 +0,0 @@
----@class GhActionsConfig
-local defaultConfig = {
- --- The browser executable path to open workflow runs/jobs in
- ---@type string|nil
- browser = nil,
- --- Interval to refresh in seconds
- refresh_interval = 10,
- --- How much workflow runs and jobs should be indented
- indent = 2,
- --- Allowed hosts to fetch data from, github.com is always allowed
- --- @type string[]
- allowed_hosts = {},
- ---@class GhActionsIcons
- icons = {
- workflow_dispatch = '⚡️',
- ---@class GhActionsIconsConclusion
- conclusion = {
- success = '✓',
- failure = 'X',
- startup_failure = 'X',
- cancelled = '⊘',
- skipped = '◌',
- action_required = '⚠',
- },
- ---@class GhActionsIconsStatus
- status = {
- unknown = '?',
- pending = '○',
- queued = '○',
- requested = '○',
- waiting = '○',
- in_progress = '●',
- },
- },
- ---@class GhActionsHighlights
- highlights = {
- GhActionsRunIconSuccess = { link = 'LspDiagnosticsVirtualTextHint' },
- GhActionsRunIconFailure = { link = 'LspDiagnosticsVirtualTextError' },
- GhActionsRunIconStartup_failure = {
- link = 'LspDiagnosticsVirtualTextError',
- },
- GhActionsRunIconPending = { link = 'LspDiagnosticsVirtualTextWarning' },
- GhActionsRunIconRequested = { link = 'LspDiagnosticsVirtualTextWarning' },
- GhActionsRunIconWaiting = { link = 'LspDiagnosticsVirtualTextWarning' },
- GhActionsRunIconIn_progress = { link = 'LspDiagnosticsVirtualTextWarning' },
- GhActionsRunIconCancelled = { link = 'Comment' },
- GhActionsRunIconSkipped = { link = 'Comment' },
- GhActionsRunCancelled = { link = 'Comment' },
- GhActionsRunSkipped = { link = 'Comment' },
- GhActionsJobCancelled = { link = 'Comment' },
- GhActionsJobSkipped = { link = 'Comment' },
- GhActionsStepCancelled = { link = 'Comment' },
- GhActionsStepSkipped = { link = 'Comment' },
- },
- split = {
- relative = 'editor',
- position = 'right',
- size = 60,
- win_options = {
- wrap = false,
- number = false,
- foldlevel = nil,
- foldcolumn = '0',
- cursorcolumn = false,
- signcolumn = 'no',
- },
- },
-}
-
-local M = {
- options = defaultConfig,
-}
-
----@param opts? GhActionsConfig
-function M.setup(opts)
- opts = opts or {}
-
- M.options = vim.tbl_deep_extend('force', defaultConfig, opts)
- M.options.allowed_hosts = M.options.allowed_hosts or {}
- table.insert(M.options.allowed_hosts, 'github.com')
-end
-
-return M
diff --git a/lua/gh-actions/health.lua b/lua/gh-actions/health.lua
deleted file mode 100644
index b104961..0000000
--- a/lua/gh-actions/health.lua
+++ /dev/null
@@ -1,46 +0,0 @@
-local M = {}
-
-function M.check()
- local health = vim.health or require('health')
-
- local start = health.start or health.report_start
- local ok = health.ok or health.report_ok
- local warn = health.warn or health.report_warn
- local error = health.error or health.report_error
-
- start('Checking ability to parse yaml files')
-
- local has_rust_module = pcall(require, 'gh-actions.rust')
-
- if has_rust_module then
- ok('Found rust module')
- else
- warn('No rust module found')
- end
-
- local has_yq_installed = vim.fn.executable('yq') == 1
-
- if has_yq_installed then
- ok('Found yq executable')
- else
- warn('No yq executable found')
- end
-
- if has_rust_module or has_yq_installed then
- ok('Found yaml parser')
- else
- error('No yaml parser found')
- end
-
- start('Checking for GitHub token')
-
- local k, token = pcall(require('gh-actions.github').get_github_token)
-
- if k and token then
- ok('Found GitHub token')
- else
- error('No GitHub token found')
- end
-end
-
-return M
diff --git a/lua/gh-actions/init.lua b/lua/gh-actions/init.lua
index 02b32eb..7ae8fae 100644
--- a/lua/gh-actions/init.lua
+++ b/lua/gh-actions/init.lua
@@ -1,361 +1,13 @@
-local M = {
- setup_called = false,
- init_root = '',
- ---@type uv_timer_t|nil
- timer = nil,
- timers = 0,
-}
+local gha = setmetatable({}, { __index = require('pipeline') })
----@param opts? GhActionsConfig
-function M.setup(opts)
- opts = opts or {}
+---@override
+function gha.setup(...)
+ vim.notify_once(
+ 'topaxi/gh-actions.nvim is deprecated, use topaxi/pipeline.nvim instead',
+ vim.log.levels.WARN
+ )
- M.init_root = vim.fn.getcwd()
-
- M.setup_called = true
-
- require('gh-actions.config').setup(opts)
- require('gh-actions.ui').setup()
- require('gh-actions.command').setup()
-end
-
-local function is_host_allowed(host)
- local config = require('gh-actions.config')
-
- for _, allowed_host in ipairs(config.options.allowed_hosts) do
- if host == allowed_host then
- return true
- end
- end
-
- return false
-end
-
---TODO Only periodically fetch all workflows
--- then fetch runs for a single workflow (tabs/expandable)
--- Maybe periodically fetch all workflow runs to update
--- "toplevel" workflow states
---TODO Maybe send lsp progress events when fetching, to interact
--- with fidget.nvim
-local function fetch_data()
- local gh = require('gh-actions.github')
- local store = require('gh-actions.store')
- local server, repo = gh.get_current_repository()
-
- store.update_state(function(state)
- state.repo = repo
- state.server = server
- end)
-
- if not is_host_allowed(server) then
- return
- end
-
- gh.get_workflows(server, repo, {
- callback = function(workflows)
- store.update_state(function(state)
- state.workflows = workflows
- end)
- end,
- })
-
- gh.get_repository_workflow_runs(server, repo, 100, {
- callback = function(workflow_runs)
- local utils = require('gh-actions.utils')
- local old_workflow_runs = store.get_state().workflow_runs
-
- store.update_state(function(state)
- state.workflow_runs = workflow_runs
- end)
-
- local running_workflows = utils.uniq(
- function(run)
- return run.id
- end,
- vim.tbl_filter(function(run)
- return run.status ~= 'completed' and run.status ~= 'skipped'
- end, { unpack(old_workflow_runs), unpack(workflow_runs) })
- )
-
- for _, run in ipairs(running_workflows) do
- gh.get_workflow_run_jobs(server, repo, run.id, 20, {
- callback = function(jobs)
- store.update_state(function(state)
- state.workflow_jobs[run.id] = jobs
- end)
- end,
- })
- end
- end,
- })
-end
-
-function M.start_polling()
- M.timers = M.timers + 1
-
- if not M.timer then
- M.timer = vim.loop.new_timer()
- M.timer:start(
- 0,
- require('gh-actions.config').options.refresh_interval * 1000,
- vim.schedule_wrap(fetch_data)
- )
- end
-end
-
-function M.stop_polling()
- M.timers = M.timers - 1
-
- if M.timers == 0 then
- M.timer:stop()
- M.timer:close()
- M.timer = nil
- end
-end
-
-local function now()
- return os.time()
-end
-
-local WORKFLOW_CONFIG_CACHE_TTL_S = 10
-
----TODO We should run this after fetching the workflows instead of within the state update event
----@param state GhActionsState
-function M.update_workflow_configs(state)
- local gh = require('gh-actions.github')
- local n = now()
-
- for _, workflow in ipairs(state.workflows) do
- if
- not state.workflow_configs[workflow.id]
- or (n - state.workflow_configs[workflow.id].last_read)
- > WORKFLOW_CONFIG_CACHE_TTL_S
- then
- state.workflow_configs[workflow.id] = {
- last_read = n,
- config = gh.get_workflow_config(workflow.path),
- }
- end
- end
-end
-
----@param opts { prompt: string, title: string, default_value: string, on_submit: fun(value: string) }
-local function text(opts)
- local Input = require('nui.input')
-
- return Input({
- relative = 'editor',
- position = '50%',
- size = {
- width = #opts.prompt + 32,
- },
- border = {
- style = 'rounded',
- text = { top = opts.title },
- },
- }, {
- prompt = opts.prompt,
- default_value = opts.default_value,
- on_submit = opts.on_submit,
- })
-end
-
----@param opts { prompt: string, title: string, options: string[], on_submit: fun(value: { text: string }) }
-local function menu(opts)
- local Menu = require('nui.menu')
- local lines = { Menu.separator(opts.prompt) }
-
- for _, option in ipairs(opts.options) do
- table.insert(lines, Menu.item(option))
- end
-
- return Menu({
- relative = 'editor',
- position = '50%',
- size = {
- width = #opts.prompt + 32,
- },
- border = {
- style = 'rounded',
- text = { top = opts.title },
- },
- }, {
- lines = lines,
- on_submit = opts.on_submit,
- keymap = {
- focus_next = { 'j', '', '' },
- focus_prev = { 'k', '', '' },
- close = { '', '' },
- submit = { '', '' },
- },
- })
-end
-
-function M.open()
- local ui = require('gh-actions.ui')
- local store = require('gh-actions.store')
- local utils = require('gh-actions.utils')
-
- ui.open()
- ui.split:map('n', 'q', M.close, { noremap = true })
-
- ui.split:map('n', 'gw', function()
- local workflow = ui.get_workflow()
-
- if workflow then
- utils.open(workflow.html_url)
-
- return
- end
- end, { noremap = true })
-
- ui.split:map('n', 'gr', function()
- local workflow_run = ui.get_workflow_run()
-
- if workflow_run then
- utils.open(workflow_run.html_url)
-
- return
- end
- end, { noremap = true })
-
- ui.split:map('n', 'gj', function()
- local workflow_job = ui.get_workflow_job()
-
- if workflow_job then
- utils.open(workflow_job.html_url)
-
- return
- end
- end, { noremap = true })
-
- -- TODO Move this into its own module, ui?
- ui.split:map('n', 'd', function()
- local gh = require('gh-actions.github')
- local workflow = ui.get_workflow()
-
- if workflow then
- local server = store.get_state().server
- local repo = store.get_state().repo
-
- -- TODO should we get current ref instead or show an input with the
- -- default branch or current ref preselected?
- local default_branch = require('gh-actions.git').get_default_branch()
- local workflow_config =
- require('gh-actions.yaml').read_yaml_file(workflow.path)
-
- if not workflow_config or not workflow_config.on.workflow_dispatch then
- return
- end
-
- local inputs = {}
-
- if not utils.is_nil(workflow_config.on.workflow_dispatch) then
- inputs = workflow_config.on.workflow_dispatch.inputs
- end
-
- local event = require('nui.utils.autocmd').event
- local questions = {}
- local i = 0
- local input_values = vim.empty_dict()
-
- -- TODO: Would be great to be able to cycle back to previous inputs
- local function ask_next()
- i = i + 1
-
- if #questions > 0 and i <= #questions then
- questions[i]:mount()
- else
- gh.dispatch_workflow(server, repo, workflow.id, default_branch, {
- body = { inputs = input_values or {} },
- callback = function(_res)
- utils.delay(2000, function()
- gh.get_workflow_runs(server, repo, workflow.id, 5, {
- callback = function(workflow_runs)
- store.update_state(function(state)
- state.workflow_runs = utils.uniq(function(run)
- return run.id
- end, {
- unpack(workflow_runs),
- unpack(state.workflow_runs),
- })
- end)
- end,
- })
- end)
- end,
- })
-
- if #questions == 0 then
- vim.notify(string.format('Dispatched %s', workflow.name))
- else
- -- TODO format by iterating instead of inspect
- vim.notify(
- string.format(
- 'Dispatched %s with %s',
- workflow.name,
- vim.inspect(input_values)
- )
- )
- end
- end
- end
-
- for name, input in pairs(inputs) do
- local prompt = string.format('%s: ', input.description or name)
-
- if input.type == 'choice' then
- local question = menu {
- prompt = prompt,
- title = workflow.name,
- options = input.options,
- on_submit = function(value)
- input_values[name] = value.text
- ask_next()
- end,
- }
-
- question:on(event.BufLeave, function()
- question:unmount()
- end)
-
- table.insert(questions, question)
- else
- local question = text {
- prompt = prompt,
- title = workflow.name,
- default_value = input.default,
- on_submit = function(value)
- input_values[name] = value
- ask_next()
- end,
- }
-
- question:on(event.BufLeave, function()
- question:unmount()
- end)
-
- table.insert(questions, question)
- end
- end
-
- ask_next()
- end
- end, { noremap = true })
-
- M.start_polling()
-
- --TODO: This might get called after rendering..
- store.on_update(M.update_workflow_configs)
-end
-
-function M.close()
- local ui = require('gh-actions.ui')
- local store = require('gh-actions.store')
-
- ui.close()
- M.stop_polling()
- store.off_update(M.update_workflow_configs)
+ require('pipeline').setup(...)
end
-return M
+return gha
diff --git a/lua/gh-actions/rust.lua b/lua/gh-actions/rust.lua
deleted file mode 100644
index a753268..0000000
--- a/lua/gh-actions/rust.lua
+++ /dev/null
@@ -1,8 +0,0 @@
----@class gh_actions_rust
----@field parse_yaml fun(yamlstr: string): table
----@field NIL unknown
-
----@type gh_actions_rust
-local M = require('libgh_actions_rust')
-
-return M
diff --git a/lua/gh-actions/store.lua b/lua/gh-actions/store.lua
deleted file mode 100644
index e2d57a1..0000000
--- a/lua/gh-actions/store.lua
+++ /dev/null
@@ -1,63 +0,0 @@
-local utils = require('gh-actions.utils')
-
----@class GhActionsStateWorkflowConfig
----@field last_read integer
----@field config table
-
----@class GhActionsState
----@field repo string
----@field server string
----@field workflows GhWorkflow[]
----@field workflow_runs GhWorkflowRun[]
----@field workflow_jobs table
----@field workflow_configs table
-
----@type GhActionsState
-local initialState = {
- repo = '',
- server = '',
- workflows = {},
- workflow_runs = {},
- workflow_jobs = {},
- workflow_configs = {},
-}
-
-local M = {
- _state = initialState,
- _update = {},
-}
-
----@param state GhActionsState
-local function emit_update(state)
- for _, update in ipairs(M._update) do
- update(state)
- end
-end
-
-emit_update = utils.debounced(vim.schedule_wrap(emit_update))
-
----@param fn fun(render_state: GhActionsState): GhActionsState|nil
-function M.update_state(fn)
- M._state = fn(M._state) or M._state
-
- emit_update(M._state)
-end
-
----@param fn function
-function M.on_update(fn)
- table.insert(M._update, fn)
-end
-
----@param fn function
-function M.off_update(fn)
- M._update = vim.tbl_filter(function(f)
- return f ~= fn
- end, M._update)
-end
-
----@return GhActionsState
-function M.get_state()
- return M._state
-end
-
-return M
diff --git a/lua/lualine/components/gh-actions.lua b/lua/lualine/components/gh-actions.lua
deleted file mode 100644
index 5ec19e0..0000000
--- a/lua/lualine/components/gh-actions.lua
+++ /dev/null
@@ -1,50 +0,0 @@
-local function gh()
- return require('gh-actions.github')
-end
-
-local component = require('lualine.component'):extend()
-
----@class GhActionsComponent
-local default_options = {
- icon = '',
-}
-
----@override
----@param options GhActionsComponent
-function component:init(options)
- component.super.init(self, options)
-
- self.options = vim.tbl_deep_extend('force', default_options, options or {})
- self.store = require('gh-actions.store')
- self.icons = require('gh-actions.utils.icons')
-
- local server, repo = gh().get_current_repository()
-
- if not server or not repo then
- return
- end
-
- require('gh-actions').start_polling()
-
- self.store.on_update(function()
- require('lualine').refresh()
- end)
-end
-
----@override
-function component:update_status()
- local state = self.store.get_state()
-
- local latest_workflow_run = state.workflow_runs and state.workflow_runs[1]
- or {}
-
- if not latest_workflow_run.status then
- return ''
- end
-
- return self.icons.get_workflow_run_icon(latest_workflow_run)
- .. ' '
- .. latest_workflow_run.name
-end
-
-return component
diff --git a/lua/lualine/components/pipeline.lua b/lua/lualine/components/pipeline.lua
new file mode 100644
index 0000000..341f2bf
--- /dev/null
+++ b/lua/lualine/components/pipeline.lua
@@ -0,0 +1,56 @@
+---@class pipeline.lualine.Component
+---@field protected super { init: fun(self: table, options: table) }
+local Component = require('lualine.component'):extend()
+
+---@class pipeline.lualine.ComponentOptions
+local default_options = {
+ icon = '',
+ ---@param component pipeline.lualine.Component
+ ---@param state pipeline.State
+ ---@return string
+ format = function(component, state)
+ local latest_run = state.latest_run
+
+ if not latest_run or not latest_run.status then
+ return ''
+ end
+
+ return component.icons.get_workflow_run_icon(latest_run)
+ .. ' '
+ .. latest_run.name
+ end,
+
+ on_click = function()
+ require('pipeline').toggle()
+ end,
+}
+
+---@override
+---@param options pipeline.lualine.ComponentOptions
+function Component:init(options)
+ self.options = vim.tbl_deep_extend('force', default_options, options or {})
+
+ Component.super.init(self, self.options)
+
+ self.store = require('pipeline.store')
+ self.icons = require('pipeline.utils.icons')
+
+ local server, repo = require('pipeline.git').get_current_repository()
+
+ if not server or not repo then
+ return
+ end
+
+ require('pipeline').start_polling()
+
+ self.store.on_update(function()
+ require('lualine').refresh()
+ end)
+end
+
+---@override
+function Component:update_status()
+ return self.options.format(self, self.store.get_state())
+end
+
+return Component
diff --git a/lua/pipeline/command.lua b/lua/pipeline/command.lua
new file mode 100644
index 0000000..81ac07a
--- /dev/null
+++ b/lua/pipeline/command.lua
@@ -0,0 +1,43 @@
+local M = {}
+
+local function handle_pipeline_command(a)
+ local pipeline = require('pipeline')
+
+ if a.name == 'GhActions' then
+ vim.notify_once(
+ 'GhActions command is deprecated, use Pipeline instead',
+ vim.log.levels.WARN
+ )
+ end
+
+ local action = a.fargs[1] or 'toggle'
+
+ if action == 'open' then
+ return pipeline.open()
+ elseif action == 'close' then
+ return pipeline.close()
+ elseif action == 'toggle' then
+ return pipeline.toggle()
+ end
+end
+
+local function completion_customlist()
+ return {
+ 'open',
+ 'close',
+ 'toggle',
+ }
+end
+
+function M.setup()
+ vim.api.nvim_create_user_command('Pipeline', handle_pipeline_command, {
+ nargs = '?',
+ complete = completion_customlist,
+ })
+ vim.api.nvim_create_user_command('GhActions', handle_pipeline_command, {
+ nargs = '?',
+ complete = completion_customlist,
+ })
+end
+
+return M
diff --git a/lua/pipeline/config.lua b/lua/pipeline/config.lua
new file mode 100644
index 0000000..397f6cf
--- /dev/null
+++ b/lua/pipeline/config.lua
@@ -0,0 +1,115 @@
+---@class pipeline.Config
+local defaultConfig = {
+ --- The browser executable path to open workflow runs/jobs in
+ ---@type string|nil
+ browser = nil,
+ --- How much workflow runs and jobs should be indented
+ indent = 2,
+ --- Provider options
+ ---@class pipeline.config.Providers
+ ---@field github? pipeline.providers.github.rest.Options
+ ---@field gitlab? pipeline.providers.gitlab.graphql.Options
+ providers = {
+ github = {},
+ gitlab = {},
+ },
+ --- Allowed hosts to fetch data from, github.com is always allowed
+ --- @type string[]
+ allowed_hosts = {},
+ ---@class pipeline.config.Icons
+ icons = {
+ workflow_dispatch = '⚡️',
+ ---@class pipeline.config.IconConclusion
+ conclusion = {
+ success = '✓',
+ failure = 'X',
+ startup_failure = 'X',
+ cancelled = '⊘',
+ skipped = '◌',
+ action_required = '⚠',
+ },
+ ---@class pipeline.config.IconStatus
+ status = {
+ unknown = '?',
+ pending = '○',
+ queued = '○',
+ requested = '○',
+ waiting = '○',
+ in_progress = '●',
+ },
+ },
+ ---@class pipeline.config.Highlights
+ highlights = {
+ ---@type vim.api.keyset.highlight
+ PipelineRunIconSuccess = { link = 'LspDiagnosticsVirtualTextHint' },
+ ---@type vim.api.keyset.highlight
+ PipelineRunIconFailure = { link = 'LspDiagnosticsVirtualTextError' },
+ ---@type vim.api.keyset.highlight
+ PipelineRunIconStartup_failure = {
+ link = 'LspDiagnosticsVirtualTextError',
+ },
+ ---@type vim.api.keyset.highlight
+ PipelineRunIconPending = { link = 'LspDiagnosticsVirtualTextWarning' },
+ ---@type vim.api.keyset.highlight
+ PipelineRunIconRequested = { link = 'LspDiagnosticsVirtualTextWarning' },
+ ---@type vim.api.keyset.highlight
+ PipelineRunIconWaiting = { link = 'LspDiagnosticsVirtualTextWarning' },
+ ---@type vim.api.keyset.highlight
+ PipelineRunIconIn_progress = { link = 'LspDiagnosticsVirtualTextWarning' },
+ ---@type vim.api.keyset.highlight
+ PipelineRunIconCancelled = { link = 'Comment' },
+ ---@type vim.api.keyset.highlight
+ PipelineRunIconSkipped = { link = 'Comment' },
+ ---@type vim.api.keyset.highlight
+ PipelineRunCancelled = { link = 'Comment' },
+ ---@type vim.api.keyset.highlight
+ PipelineRunSkipped = { link = 'Comment' },
+ ---@type vim.api.keyset.highlight
+ PipelineJobCancelled = { link = 'Comment' },
+ ---@type vim.api.keyset.highlight
+ PipelineJobSkipped = { link = 'Comment' },
+ ---@type vim.api.keyset.highlight
+ PipelineStepCancelled = { link = 'Comment' },
+ ---@type vim.api.keyset.highlight
+ PipelineStepSkipped = { link = 'Comment' },
+ },
+ ---@type nui_split_options
+ split = {
+ relative = 'editor',
+ position = 'right',
+ size = 60,
+ win_options = {
+ wrap = false,
+ number = false,
+ foldlevel = nil,
+ foldcolumn = '0',
+ cursorcolumn = false,
+ signcolumn = 'no',
+ },
+ },
+}
+
+local M = {
+ options = defaultConfig,
+}
+
+---@param opts? pipeline.Config
+function M.setup(opts)
+ opts = opts or {}
+
+ M.options = vim.tbl_deep_extend('force', defaultConfig, opts)
+ M.options.allowed_hosts = M.options.allowed_hosts or {}
+ table.insert(M.options.allowed_hosts, 'github.com')
+end
+
+function M.is_host_allowed(host)
+ for _, allowed_host in ipairs(M.options.allowed_hosts) do
+ if host == allowed_host then
+ return true
+ end
+ end
+
+ return false
+end
+
+return M
diff --git a/lua/gh-actions/git.lua b/lua/pipeline/git.lua
similarity index 50%
rename from lua/gh-actions/git.lua
rename to lua/pipeline/git.lua
index 3df65a6..f47bcaa 100644
--- a/lua/gh-actions/git.lua
+++ b/lua/pipeline/git.lua
@@ -5,6 +5,38 @@ local function create_job(job)
return require('plenary.job'):new(job)
end
+---@param str string
+---@return string
+local function strip_git_suffix(str)
+ if str:sub(-4) == '.git' then
+ return str:sub(1, -5)
+ end
+
+ return str
+end
+
+---@return string, string
+---@nodiscard
+function M.get_current_repository()
+ local origin_url_job = create_job {
+ command = 'git',
+ args = {
+ 'config',
+ '--get',
+ 'remote.origin.url',
+ },
+ }
+
+ origin_url_job:sync()
+
+ local origin_url = table.concat(origin_url_job:result(), '')
+
+ local server, repo =
+ strip_git_suffix(origin_url):match('([^@/:]+)[:/]([^/]+/[^/]+)$')
+
+ return server, repo
+end
+
function M.get_current_branch()
local job = create_job {
command = 'git',
diff --git a/lua/pipeline/health.lua b/lua/pipeline/health.lua
new file mode 100644
index 0000000..b0fc737
--- /dev/null
+++ b/lua/pipeline/health.lua
@@ -0,0 +1,34 @@
+local M = {}
+
+function M.check()
+ local health = vim.health
+
+ health.start('Checking ability to parse yaml files')
+
+ local has_native_module = pcall(require, 'pipeline_native.yaml')
+
+ if has_native_module then
+ health.ok('Found native module')
+ else
+ health.warn('No native module found')
+ end
+
+ local has_yq_installed = vim.fn.executable('yq') == 1
+
+ if has_yq_installed then
+ health.ok('Found yq executable')
+ else
+ health.warn('No yq executable found')
+ end
+
+ if has_native_module or has_yq_installed then
+ health.ok('Found yaml parser')
+ else
+ health.error('No yaml parser found')
+ end
+
+ require('pipeline.providers.github.rest.health').check()
+ require('pipeline.providers.gitlab.graphql.health').check()
+end
+
+return M
diff --git a/lua/pipeline/init.lua b/lua/pipeline/init.lua
new file mode 100644
index 0000000..4f1434e
--- /dev/null
+++ b/lua/pipeline/init.lua
@@ -0,0 +1,156 @@
+local M = {
+ init_root = '',
+}
+
+---@param opts? pipeline.Config
+function M.setup(opts)
+ opts = opts or {}
+
+ M.init_root = vim.fn.getcwd()
+
+ require('pipeline.config').setup(opts)
+ require('pipeline.ui').setup()
+ require('pipeline.command').setup()
+
+ M.setup_provider()
+end
+
+function M.setup_provider()
+ if M.pipeline then
+ return
+ end
+
+ local config = require('pipeline.config')
+ local store = require('pipeline.store')
+
+ M.pipeline = require('pipeline.providers.provider'):new(config.options, store)
+ for provider, provider_options in pairs(config.options.providers) do
+ local Provider = require('pipeline.providers')[provider]
+
+ if Provider.detect() then
+ M.pipeline = Provider:new(config.options, store, provider_options)
+ end
+ end
+end
+
+function M.start_polling()
+ M.pipeline:listen()
+end
+
+function M.stop_polling()
+ M.pipeline:close()
+end
+
+local function now()
+ return os.time()
+end
+
+local WORKFLOW_CONFIG_CACHE_TTL_S = 10
+
+---TODO We should run this after fetching the workflows instead of within the state update event
+---@param state pipeline.State
+function M.update_workflow_configs(state)
+ local gh_utils = require('pipeline.providers.github.utils')
+ local n = now()
+
+ for _, pipeline in ipairs(state.pipelines) do
+ if
+ not state.workflow_configs[pipeline.pipeline_id]
+ or (n - state.workflow_configs[pipeline.pipeline_id].last_read)
+ > WORKFLOW_CONFIG_CACHE_TTL_S
+ then
+ state.workflow_configs[pipeline.pipeline_id] = {
+ last_read = n,
+ config = gh_utils.get_workflow_config(pipeline.meta.workflow_path),
+ }
+ end
+ end
+end
+
+---@param pipeline_object pipeline.PipelineObject|nil
+local function open_pipeline_url(pipeline_object)
+ if not pipeline_object then
+ return
+ end
+
+ if type(pipeline_object.url) ~= 'string' or pipeline_object.url == '' then
+ return
+ end
+
+ require('pipeline.utils').open(pipeline_object.url)
+end
+
+function M.open()
+ local ui = require('pipeline.ui')
+ local store = require('pipeline.store')
+
+ ui.open()
+ ui.split:map('n', 'q', M.close, { noremap = true })
+
+ ui.split:map('n', 'gp', function()
+ open_pipeline_url(ui.get_pipeline())
+ end, { noremap = true, desc = 'Open pipeline URL' })
+
+ ui.split:map('n', 'gw', function()
+ vim.notify(
+ 'Keybind gw to jump to workflow is deprecated, use gp instead',
+ vim.log.levels.WARN
+ )
+
+ open_pipeline_url(ui.get_pipeline())
+ end, { noremap = true, desc = 'Open pipeline URL (deprecated)' })
+
+ ui.split:map('n', 'gr', function()
+ open_pipeline_url(ui.get_run())
+ end, { noremap = true, desc = 'Open pipeline run URL' })
+
+ ui.split:map('n', 'gj', function()
+ open_pipeline_url(ui.get_job())
+ end, { noremap = true, desc = 'Open pipeline job URL' })
+
+ ui.split:map('n', 'gs', function()
+ open_pipeline_url(ui.get_step())
+ end, { noremap = true, desc = 'Open pipeline step URL' })
+
+ ui.split:map('n', 'd', function()
+ M.pipeline:dispatch(ui.get_pipeline())
+ end, { noremap = true, desc = 'Dispatch pipeline run' })
+
+ ui.split:map('n', 'rr', function()
+ M.pipeline:retry(ui.get_run())
+ end, { noremap = true, desc = 'Retry pipeline run' })
+
+ ui.split:map('n', 'rj', function()
+ M.pipeline:retry(ui.get_job())
+ end, { noremap = true, desc = 'Retry pipeline job' })
+
+ ui.split:map('n', 'rs', function()
+ M.pipeline:retry(ui.get_step())
+ end, { noremap = true, desc = 'Retry pipeline step' })
+
+ M.start_polling()
+
+ --TODO: This might get called after rendering..
+ store.on_update(M.update_workflow_configs)
+end
+
+function M.close()
+ local ui = require('pipeline.ui')
+ local store = require('pipeline.store')
+
+ ui.close()
+ M.stop_polling()
+ store.off_update(M.update_workflow_configs)
+end
+
+function M.toggle()
+ local ui = require('pipeline.ui')
+
+ if ui.split.winid then
+ return M.close()
+ else
+ return M.open()
+ end
+end
+
+return M
diff --git a/lua/gh-actions/github.lua b/lua/pipeline/providers/github/rest/_api.lua
similarity index 74%
rename from lua/gh-actions/github.lua
rename to lua/pipeline/providers/github/rest/_api.lua
index c7defa2..b0f9eec 100644
--- a/lua/gh-actions/github.lua
+++ b/lua/pipeline/providers/github/rest/_api.lua
@@ -1,71 +1,9 @@
-local M = {}
-
----@param str string
----@return string
-local function strip_git_suffix(str)
- if str:sub(-4) == '.git' then
- return str:sub(1, -5)
- end
-
- return str
+local function gh_utils()
+ return require('pipeline.providers.github.utils')
end
-function M.get_current_repository()
- local job = require('plenary.job')
- local origin_url_job = job:new {
- command = 'git',
- args = {
- 'config',
- '--get',
- 'remote.origin.url',
- },
- }
-
- origin_url_job:sync()
-
- local origin_url = table.concat(origin_url_job:result(), '')
-
- return strip_git_suffix(origin_url):match('([^@/:]+)[:/]([^/]+/[^/]+)$')
-end
-
----@param cmd? string
----@param server? string
----@return string|nil
-local function get_token_from_gh_cli(cmd, server)
- local has_gh_installed = vim.fn.executable('gh') == 1
- if not has_gh_installed and not cmd then
- return nil
- end
-
- local res
- if cmd then
- res = vim.fn.system(cmd)
- else
- local gh_enterprise_flag = ''
- if server ~= nil and server ~= '' then
- gh_enterprise_flag = ' --hostname ' .. vim.fn.shellescape(server)
- end
- res = vim.fn.system('gh auth token' .. gh_enterprise_flag)
- end
-
- local token = string.gsub(res or '', '\n', '')
-
- if token == '' then
- return nil
- end
-
- return token
-end
-
----@param cmd? string
----@param server? string
----@return string
-function M.get_github_token(cmd, server)
- return vim.env.GITHUB_TOKEN
- or get_token_from_gh_cli(cmd, server)
- -- TODO: We could also ask for the token here via nui
- or assert(nil, 'No GITHUB_TOKEN found in env and no gh cli config found')
-end
+---@class pipeline.providers.github.rest.Api
+local M = {}
---@param server string
---@param path string
@@ -99,7 +37,7 @@ function M.fetch(server, path, opts)
headers = {
Authorization = string.format(
'Bearer %s',
- M.get_github_token(nil, server)
+ gh_utils().get_github_token(nil, server)
),
},
})
@@ -212,7 +150,7 @@ end
---@param server string
---@param repo string
----@param workflow_id integer
+---@param workflow_id integer|string
---@param per_page? integer
---@param opts? { callback?: fun(workflow_runs: GhWorkflowRun[]): any }
function M.get_workflow_runs(server, repo, workflow_id, per_page, opts)
@@ -229,7 +167,7 @@ end
---@param server string
---@param repo string
----@param workflow_id integer
+---@param workflow_id integer|string
---@param ref string
---@param opts? table
function M.dispatch_workflow(server, repo, workflow_id, ref, opts)
@@ -303,21 +241,4 @@ function M.get_workflow_run_jobs(server, repo, workflow_run_id, per_page, opts)
)
end
----@param path string
----@return table
-function M.get_workflow_config(path)
- path = vim.fn.expand(path)
-
- local utils = require('gh-actions.utils')
- local workflow_yaml = utils.read_file(path) or ''
- local config = {
- on = {
- workflow_dispatch = workflow_yaml:find('workflow_dispatch'),
- },
- }
-
- ---@cast config table
- return config
-end
-
return M
diff --git a/lua/pipeline/providers/github/rest/_mapper.lua b/lua/pipeline/providers/github/rest/_mapper.lua
new file mode 100644
index 0000000..db362c4
--- /dev/null
+++ b/lua/pipeline/providers/github/rest/_mapper.lua
@@ -0,0 +1,66 @@
+---@class pipeline.providers.github.rest.Mapper
+local M = {}
+
+---By default the workflow.html_url points to the workflow definition file.
+---We want to jump to the UI of all the workflow runs instead.
+---Example:
+--- input: https://github.com/topaxi/pipeline.nvim/blob/main/.github/workflows/dispatch-echo.yaml
+--- output: https://github.com/topaxi/pipeline.nvim/actions/workflows/dispatch-echo.yaml
+---@param workflow GhWorkflow
+local function workflow_url(workflow)
+ return workflow.html_url:gsub('blob/main/%.github', 'actions')
+end
+
+---@class pipeline.providers.github.rest.Pipeline: pipeline.Pipeline
+---@field meta { workflow_path: string }
+
+---@param workflow GhWorkflow
+---@return pipeline.providers.github.rest.Pipeline
+function M.to_pipeline(workflow)
+ return {
+ pipeline_id = workflow.id,
+ name = workflow.name,
+ url = workflow_url(workflow),
+ meta = { workflow_path = workflow.path },
+ }
+end
+
+---@param workflow_run GhWorkflowRun
+---@return pipeline.Run
+function M.to_run(workflow_run)
+ return {
+ run_id = workflow_run.id,
+ pipeline_id = workflow_run.workflow_id,
+ name = workflow_run.head_commit.message:gsub('\n.*', ''),
+ url = workflow_run.html_url,
+ status = workflow_run.status,
+ conclusion = workflow_run.conclusion,
+ }
+end
+
+---@param workflow_job GhWorkflowRunJob
+---@return pipeline.Job
+function M.to_job(workflow_job)
+ return {
+ job_id = workflow_job.id,
+ run_id = workflow_job.run_id,
+ name = workflow_job.name,
+ status = workflow_job.status,
+ conclusion = workflow_job.conclusion,
+ }
+end
+
+---@param job_id integer
+---@param workflow_job_step GhWorkflowRunJobStep
+---@return pipeline.Step
+function M.to_step(job_id, workflow_job_step)
+ return {
+ step_id = string.format('%s:%s', job_id, workflow_job_step.number),
+ job_id = job_id,
+ name = workflow_job_step.name,
+ status = workflow_job_step.status,
+ conclusion = workflow_job_step.conclusion,
+ }
+end
+
+return M
diff --git a/lua/pipeline/providers/github/rest/health.lua b/lua/pipeline/providers/github/rest/health.lua
new file mode 100644
index 0000000..14f1489
--- /dev/null
+++ b/lua/pipeline/providers/github/rest/health.lua
@@ -0,0 +1,18 @@
+local M = {}
+
+function M.check()
+ local health = vim.health
+
+ health.start('Github REST provider')
+
+ local k, token =
+ pcall(require('pipeline.providers.github.utils').get_github_token)
+
+ if k and token then
+ health.ok('Found GitHub token')
+ else
+ health.error('No GitHub token found')
+ end
+end
+
+return M
diff --git a/lua/pipeline/providers/github/rest/init.lua b/lua/pipeline/providers/github/rest/init.lua
new file mode 100644
index 0000000..8e01c2a
--- /dev/null
+++ b/lua/pipeline/providers/github/rest/init.lua
@@ -0,0 +1,269 @@
+local utils = require('pipeline.utils')
+local Provider = require('pipeline.providers.provider')
+
+local function git()
+ return require('pipeline.git')
+end
+
+local function gh_api()
+ return require('pipeline.providers.github.rest._api')
+end
+
+---@class pipeline.providers.github.rest.Options
+---@field refresh_interval? number
+local defaultOptions = {
+ refresh_interval = 10,
+}
+
+---@class pipeline.providers.github.rest.Provider: pipeline.Provider
+---@field protected opts pipeline.providers.github.rest.Options
+---@field private server string
+---@field private repo string
+local GithubRestProvider = Provider:extend()
+
+function GithubRestProvider.detect()
+ if not utils.file_exists('.github/workflows') then
+ return false
+ end
+
+ local config = require('pipeline.config')
+ local server, repo = git().get_current_repository()
+
+ if not config.is_host_allowed(server) then
+ return
+ end
+
+ return server ~= nil and repo ~= nil
+end
+
+---@param opts pipeline.providers.github.rest.Options
+function GithubRestProvider:init(opts)
+ local server, repo = git().get_current_repository()
+
+ self.opts = vim.tbl_deep_extend('force', defaultOptions, opts)
+ self.server = server
+ self.repo = repo
+
+ self.store.update_state(function(state)
+ state.title = string.format('Github Workflows for %s', repo)
+ state.server = server
+ state.repo = repo
+ end)
+end
+
+--TODO Only periodically fetch all workflows
+-- then fetch runs for a single workflow (tabs/expandable)
+-- Maybe periodically fetch all workflow runs to update
+-- "toplevel" workflow states
+--TODO Maybe send lsp progress events when fetching, to interact
+-- with fidget.nvim
+function GithubRestProvider:fetch()
+ local Mapper = require('pipeline.providers.github.rest._mapper')
+
+ gh_api().get_workflows(self.server, self.repo, {
+ callback = function(workflows)
+ self.store.update_state(function(state)
+ state.pipelines = vim.tbl_map(Mapper.to_pipeline, workflows)
+ end)
+ end,
+ })
+
+ gh_api().get_repository_workflow_runs(self.server, self.repo, 100, {
+ callback = function(workflow_runs)
+ ---@type pipeline.Run[]
+ local runs = vim.tbl_map(Mapper.to_run, workflow_runs)
+ ---@type pipeline.Run[]
+ local old_runs = vim.iter(self.store.get_state().runs):flatten():totable()
+
+ self.store.update_state(function(state)
+ state.latest_run = runs[1]
+ state.runs = utils.group_by(function(run)
+ return run.pipeline_id
+ end, runs)
+ end)
+
+ local running_workflows = utils.uniq(
+ function(run)
+ return run.run_id
+ end,
+ vim.tbl_filter(function(run)
+ return run.status ~= 'completed' and run.status ~= 'skipped'
+ end, { unpack(runs), unpack(old_runs) })
+ )
+
+ for _, run in ipairs(running_workflows) do
+ gh_api().get_workflow_run_jobs(self.server, self.repo, run.run_id, 20, {
+ callback = function(jobs)
+ self.store.update_state(function(state)
+ state.jobs[run.run_id] = vim.tbl_map(Mapper.to_job, jobs)
+
+ for _, job in ipairs(jobs) do
+ state.steps[job.id] = vim.tbl_map(function(step)
+ return Mapper.to_step(job.id, step)
+ end, job.steps)
+ end
+ end)
+ end,
+ })
+ end
+ end,
+ })
+end
+
+---@param pipeline pipeline.providers.github.rest.Pipeline|nil
+function GithubRestProvider:dispatch(pipeline)
+ if not pipeline then
+ return
+ end
+
+ local store = require('pipeline.store')
+
+ if pipeline then
+ local server = store.get_state().server
+ local repo = store.get_state().repo
+
+ -- TODO should we get current ref instead or show an input with the
+ -- default branch or current ref preselected?
+ local default_branch = require('pipeline.git').get_default_branch()
+ ---@type pipeline.providers.github.WorkflowDef|nil
+ local workflow_config =
+ require('pipeline.yaml').read_yaml_file(pipeline.meta.workflow_path)
+
+ if not workflow_config or not workflow_config.on.workflow_dispatch then
+ return
+ end
+
+ ---@type pipeline.providers.github.WorkflowDef.DispatchInputs
+ local inputs = {}
+
+ if not utils.is_nil(workflow_config.on.workflow_dispatch) then
+ ---@type pipeline.providers.github.WorkflowDef.DispatchInputs
+ inputs = workflow_config.on.workflow_dispatch.inputs
+ end
+
+ local questions = {}
+ local i = 0
+ local input_values = vim.empty_dict()
+
+ -- TODO: Would be great to be able to cycle back to previous inputs
+ local function ask_next()
+ i = i + 1
+
+ if #questions > 0 and i <= #questions then
+ questions[i]:mount()
+ else
+ gh_api().dispatch_workflow(
+ server,
+ repo,
+ pipeline.pipeline_id,
+ default_branch,
+ {
+ body = { inputs = input_values or {} },
+ callback = function(_res)
+ utils.delay(2000, function()
+ gh_api().get_workflow_runs(
+ server,
+ repo,
+ pipeline.pipeline_id,
+ 5,
+ {
+ callback = function(workflow_runs)
+ local Mapper =
+ require('pipeline.providers.github.rest._mapper')
+ local runs = vim.tbl_map(Mapper.to_run, workflow_runs)
+
+ store.update_state(function(state)
+ state.runs = utils.group_by(
+ function(run)
+ return run.pipeline_id
+ end,
+ utils.uniq(function(run)
+ return run.run_id
+ end, {
+ unpack(runs),
+ unpack(vim.iter(state.runs):flatten():totable()),
+ })
+ )
+ end)
+ end,
+ }
+ )
+ end)
+ end,
+ }
+ )
+
+ if #questions == 0 then
+ vim.notify(string.format('Dispatched %s', pipeline.name))
+ else
+ -- TODO format by iterating instead of inspect
+ vim.notify(
+ string.format(
+ 'Dispatched %s with %s',
+ pipeline.name,
+ vim.inspect(input_values)
+ )
+ )
+ end
+ end
+ end
+
+ for name, input in pairs(inputs) do
+ local prompt = string.format('%s: ', input.description or name)
+
+ if input.type == 'choice' then
+ local question = require('pipeline.ui.components.select') {
+ prompt = prompt,
+ title = pipeline.name,
+ options = input.options,
+ on_submit = function(value)
+ input_values[name] = value.text
+ ask_next()
+ end,
+ }
+
+ question:on('BufLeave', function()
+ question:unmount()
+ end)
+
+ table.insert(questions, question)
+ else
+ local question = require('pipeline.ui.components.input') {
+ prompt = prompt,
+ title = pipeline.name,
+ default_value = input.default,
+ on_submit = function(value)
+ input_values[name] = value
+ ask_next()
+ end,
+ }
+
+ question:on('BufLeave', function()
+ question:unmount()
+ end)
+
+ table.insert(questions, question)
+ end
+ end
+
+ ask_next()
+ end
+end
+
+function GithubRestProvider:connect()
+ self.timer = vim.loop.new_timer()
+ self.timer:start(
+ 0,
+ self.opts.refresh_interval * 1000,
+ vim.schedule_wrap(function()
+ self:fetch()
+ end)
+ )
+end
+
+function GithubRestProvider:disconnect()
+ self.timer:stop()
+ self.timer = nil
+end
+
+return GithubRestProvider
diff --git a/lua/pipeline/providers/github/types.lua b/lua/pipeline/providers/github/types.lua
new file mode 100644
index 0000000..224ff73
--- /dev/null
+++ b/lua/pipeline/providers/github/types.lua
@@ -0,0 +1,20 @@
+---@meta
+
+---@class pipeline.providers.github.WorkflowDef
+---@field on? { workflow_dispatch?: { inputs?: pipeline.providers.github.WorkflowDef.DispatchInputs } }
+
+---@alias pipeline.providers.github.WorkflowDef.DispatchInputs table
+
+---@class pipeline.providers.github.WorkflowDef.DispatchInputBase
+---@field description? string
+---@field required? boolean
+---@field default? string
+
+---@class pipeline.providers.github.WorkflowDef.DispatchInputString: pipeline.providers.github.WorkflowDef.DispatchInputBase
+---@field type? 'string'
+
+---@class pipeline.providers.github.WorkflowDef.DispatchInputChoice: pipeline.providers.github.WorkflowDef.DispatchInputBase
+---@field type 'choice'
+---@field options? string[]
+
+---@alias pipeline.providers.github.WorkflowDef.DispatchInput pipeline.providers.github.WorkflowDef.DispatchInputString | pipeline.providers.github.WorkflowDef.DispatchInputChoice
diff --git a/lua/pipeline/providers/github/utils.lua b/lua/pipeline/providers/github/utils.lua
new file mode 100644
index 0000000..4c3e28c
--- /dev/null
+++ b/lua/pipeline/providers/github/utils.lua
@@ -0,0 +1,60 @@
+---@class pipeline.providers.github.Utils
+local M = {}
+
+---@param cmd? string
+---@param server? string
+---@return string|nil
+local function get_token_from_gh_cli(cmd, server)
+ local has_gh_installed = vim.fn.executable('gh') == 1
+ if not has_gh_installed and not cmd then
+ return nil
+ end
+
+ local res
+ if cmd then
+ res = vim.fn.system(cmd)
+ else
+ local gh_enterprise_flag = ''
+ if server ~= nil and server ~= '' then
+ gh_enterprise_flag = ' --hostname ' .. vim.fn.shellescape(server)
+ end
+ res = vim.fn.system('gh auth token' .. gh_enterprise_flag)
+ end
+
+ local token = string.gsub(res or '', '\n', '')
+
+ if token == '' then
+ return nil
+ end
+
+ return token
+end
+
+---@param cmd? string
+---@param server? string
+---@return string
+function M.get_github_token(cmd, server)
+ return vim.env.GITHUB_TOKEN
+ or get_token_from_gh_cli(cmd, server)
+ -- TODO: We could also ask for the token here via nui
+ or assert(nil, 'No GITHUB_TOKEN found in env and no gh cli config found')
+end
+
+---@param path string
+---@return table
+function M.get_workflow_config(path)
+ path = vim.fn.expand(path)
+
+ local utils = require('pipeline.utils')
+ local workflow_yaml = utils.read_file(path) or ''
+ local config = {
+ on = {
+ workflow_dispatch = workflow_yaml:find('workflow_dispatch'),
+ },
+ }
+
+ ---@cast config table
+ return config
+end
+
+return M
diff --git a/lua/pipeline/providers/gitlab/graphql/_api.lua b/lua/pipeline/providers/gitlab/graphql/_api.lua
new file mode 100644
index 0000000..085d631
--- /dev/null
+++ b/lua/pipeline/providers/gitlab/graphql/_api.lua
@@ -0,0 +1,109 @@
+local M = {}
+
+local pipelines_with_jobs_query = [[
+ query ($repo: ID!, $limit: Int!) {
+ project(fullPath: $repo) {
+ id
+ ciConfigPathOrDefault
+ pipelines(first: $limit) {
+ nodes {
+ id
+ name
+ commit {
+ message
+ }
+ path
+ cancelable
+ retryable
+ createdAt
+ status
+ jobs {
+ nodes {
+ id
+ name
+ status
+ manualJob
+ retryable
+ cancelable
+ stage {
+ name
+ }
+ webPath
+ }
+ }
+ }
+ }
+ }
+ }
+]]
+
+---@param job Job
+local function create_job(job)
+ return require('plenary.job'):new(job)
+end
+
+local function glab_graphql(query, variables)
+ local args = {
+ 'api',
+ 'graphql',
+ '-f',
+ 'query=' .. query,
+ }
+
+ for key, value in pairs(variables) do
+ table.insert(args, '-F')
+ table.insert(args, key .. '=' .. value)
+ end
+
+ return create_job {
+ command = 'glab',
+ args = args,
+ }
+end
+
+---@alias pipeline.providers.gitlab.graphql.CiJobStatus 'CANCELED'|'CANCELING'|'CREATED'|'FAILED'|'MANUAL'|'PENDING'|'PREPARING'|'RUNNING'|'SCHEDULED'|'SKIPPED'|'SUCCESS'|'WAITING_FOR_CALLBACK'|'WAITING_FOR_RESOURCE'
+---@alias pipeline.providers.gitlab.graphql.PipelineStatus 'CREATED'|'WAITING_FOR_RESOURCE'|'PREPARING'|'WAITING_FOR_CALLBACK'|'PENDING'|'RUNNING'|'FAILED'|'SUCCESS'|'CANCELED'|'CANCELING'|'SKIPPED'|'MANUAL'|'SCHEDULED'
+
+---@class pipeline.providers.gitlab.graphql.QueryResponseJob
+---@field id string
+---@field name string
+---@field status pipeline.providers.gitlab.graphql.CiJobStatus
+---@field manualJob boolean
+---@field retryable boolean
+---@field cancelable boolean
+---@field stage { name: string }
+---@field webPath string
+
+---@class pipeline.providers.gitlab.graphql.QueryResponsePipeline
+---@field id string
+---@field name string|nil
+---@field commit { message: string }
+---@field path string
+---@field cancelable boolean
+---@field retryable boolean
+---@field createdAt string
+---@field status pipeline.providers.gitlab.graphql.PipelineStatus
+---@field jobs { nodes: pipeline.providers.gitlab.graphql.QueryResponseJob[] }
+
+---@class pipeline.providers.gitlab.graphql.QueryResponseProject
+---@field id string
+---@field ciConfigPathOrDefault string
+---@field pipelines { nodes: pipeline.providers.gitlab.graphql.QueryResponsePipeline[] }
+
+---@class pipeline.providers.gitlab.graphql.QueryResponse
+---@field data { project: pipeline.providers.gitlab.graphql.QueryResponseProject }
+
+---@param repo string
+---@param limit number
+---@param callback fun(response: pipeline.providers.gitlab.graphql.QueryResponse)
+function M.get_project_pipelines(repo, limit, callback)
+ local query_job =
+ glab_graphql(pipelines_with_jobs_query, { repo = repo, limit = limit })
+
+ query_job:start()
+ query_job:after(function(job)
+ callback(vim.json.decode(table.concat(job:result(), '')))
+ end)
+end
+
+return M
diff --git a/lua/pipeline/providers/gitlab/graphql/_mapper.lua b/lua/pipeline/providers/gitlab/graphql/_mapper.lua
new file mode 100644
index 0000000..3252f5d
--- /dev/null
+++ b/lua/pipeline/providers/gitlab/graphql/_mapper.lua
@@ -0,0 +1,90 @@
+---@class pipeline.providers.gitlab.graphql.Mapper
+local M = {}
+
+---@type table
+local status_map = {
+ CANCELED = 'completed',
+ CANCELING = 'waiting',
+ CREATED = 'pending',
+ FAILED = 'completed',
+ MANUAL = 'unknown',
+ PENDING = 'pending',
+ PREPARING = 'queued',
+ RUNNING = 'in_progress',
+ SCHEDULED = 'queued',
+ SKIPPED = 'completed',
+ SUCCESS = 'completed',
+ WAITING_FOR_CALLBACK = 'waiting',
+ WAITING_FOR_RESOURCE = 'waiting',
+}
+
+---@type table
+local conclusion_map = {
+ CANCELED = 'cancelled',
+ CANCELING = 'cancelled',
+ CREATED = 'unknown',
+ FAILED = 'failure',
+ MANUAL = 'unknown',
+ PENDING = 'unknown',
+ PREPARING = 'unknown',
+ RUNNING = 'unknown',
+ SCHEDULED = 'unknown',
+ SKIPPED = 'skipped',
+ SUCCESS = 'success',
+ WAITING_FOR_CALLBACK = 'unknown',
+ WAITING_FOR_RESOURCE = 'unknown',
+}
+
+---@param status pipeline.providers.gitlab.graphql.CiJobStatus|pipeline.providers.gitlab.graphql.PipelineStatus
+---@return pipeline.Status
+local function map_status(status)
+ return status_map[status] or 'unknown'
+end
+
+---@param status pipeline.providers.gitlab.graphql.CiJobStatus|pipeline.providers.gitlab.graphql.PipelineStatus
+---@return pipeline.Conclusion
+local function map_conclusion(status)
+ return conclusion_map[status] or 'unknown'
+end
+
+---@class pipeline.providers.gitlab.graphql.Pipeline: pipeline.Pipeline
+---@field meta { ci_config_path: string }
+
+---@param project pipeline.providers.gitlab.graphql.QueryResponseProject
+---@return pipeline.providers.github.rest.Pipeline
+function M.to_pipeline(project)
+ return {
+ pipeline_id = project.id,
+ name = project.ciConfigPathOrDefault,
+ meta = { ci_config_path = project.ciConfigPathOrDefault },
+ }
+end
+
+---@param pipeline_id string
+---@param pipeline pipeline.providers.gitlab.graphql.QueryResponsePipeline
+---@return pipeline.Run
+function M.to_run(pipeline_id, pipeline)
+ return {
+ run_id = pipeline.id,
+ pipeline_id = pipeline_id,
+ name = pipeline.commit.message:gsub('\n.*', ''),
+ url = pipeline.path,
+ status = map_status(pipeline.status),
+ conclusion = map_conclusion(pipeline.status),
+ }
+end
+
+---@param run_id string
+---@param job pipeline.providers.gitlab.graphql.QueryResponseJob
+---@return pipeline.Job
+function M.to_job(run_id, job)
+ return {
+ job_id = job.id,
+ run_id = run_id,
+ name = job.name,
+ status = map_status(job.status),
+ conclusion = map_conclusion(job.status),
+ }
+end
+
+return M
diff --git a/lua/pipeline/providers/gitlab/graphql/health.lua b/lua/pipeline/providers/gitlab/graphql/health.lua
new file mode 100644
index 0000000..34aec79
--- /dev/null
+++ b/lua/pipeline/providers/gitlab/graphql/health.lua
@@ -0,0 +1,15 @@
+local M = {}
+
+function M.check()
+ local health = vim.health
+
+ health.start('Gitlab GraphQL provider')
+
+ if vim.fn.executable('glab') then
+ health.ok('Found glab cli')
+ else
+ health.error('glab cli not found')
+ end
+end
+
+return M
diff --git a/lua/pipeline/providers/gitlab/graphql/init.lua b/lua/pipeline/providers/gitlab/graphql/init.lua
new file mode 100644
index 0000000..d08ddce
--- /dev/null
+++ b/lua/pipeline/providers/gitlab/graphql/init.lua
@@ -0,0 +1,125 @@
+local utils = require('pipeline.utils')
+local Provider = require('pipeline.providers.provider')
+
+local function git()
+ return require('pipeline.git')
+end
+
+local function glab_api()
+ return require('pipeline.providers.gitlab.graphql._api')
+end
+
+---@class pipeline.providers.gitlab.graphql.Options
+---@field refresh_interval? number
+local defaultOptions = {
+ refresh_interval = 10,
+}
+
+---@class pipeline.providers.gitlab.graphql.Provider: pipeline.Provider
+---@field protected opts pipeline.providers.gitlab.graphql.Options
+---@field private server string
+---@field private repo string
+local GitlabGraphQLProvider = Provider:extend()
+
+function GitlabGraphQLProvider.detect()
+ if not utils.file_exists('.gitlab-ci.yml') then
+ return false
+ end
+
+ local config = require('pipeline.config')
+ local server, repo = git().get_current_repository()
+
+ if not config.is_host_allowed(server) then
+ return
+ end
+
+ return server ~= nil and repo ~= nil
+end
+
+---@param opts pipeline.providers.github.rest.Options
+function GitlabGraphQLProvider:init(opts)
+ local server, repo = git().get_current_repository()
+
+ self.opts = vim.tbl_deep_extend('force', defaultOptions, opts)
+ self.server = server
+ self.repo = repo
+
+ self.store.update_state(function(state)
+ state.title = string.format('Gitlab Pipelines for %s', repo)
+ state.server = server
+ state.repo = repo
+ end)
+end
+
+function GitlabGraphQLProvider:fetch()
+ local Mapper = require('pipeline.providers.gitlab.graphql._mapper')
+
+ glab_api().get_project_pipelines(self.repo, 10, function(response)
+ if
+ utils.is_nil(response.data) or type(response.data.project) == 'userdata'
+ then
+ -- TODO: Handle errors
+ return
+ end
+
+ local pipeline = Mapper.to_pipeline(response.data.project)
+ local runs = {
+ [pipeline.pipeline_id] = vim.tbl_map(function(node)
+ return Mapper.to_run(pipeline.pipeline_id, node)
+ end, response.data.project.pipelines.nodes),
+ }
+ local jobs = utils.group_by(
+ function(job)
+ return job.run_id
+ end,
+ vim
+ .iter(response.data.project.pipelines.nodes)
+ :map(function(node)
+ return vim.tbl_map(function(job)
+ return Mapper.to_job(node.id, job)
+ end, node.jobs.nodes)
+ end)
+ :flatten()
+ :totable()
+ )
+
+ self.store.update_state(function(state)
+ state.pipelines = { pipeline }
+ state.latest_run = runs[pipeline.pipeline_id][1]
+ state.runs = runs
+ state.jobs = jobs
+ end)
+ end)
+end
+
+---@param pipeline pipeline.providers.gitlab.graphql.Pipeline|nil
+function GitlabGraphQLProvider:dispatch(pipeline)
+ if not pipeline then
+ return
+ end
+
+ if pipeline then
+ vim.notify(
+ 'Gitlab Pipeline dispatch is not yet implemented',
+ vim.log.levels.INFO
+ )
+ end
+end
+
+function GitlabGraphQLProvider:connect()
+ self.timer = vim.loop.new_timer()
+ self.timer:start(
+ 0,
+ self.opts.refresh_interval * 1000,
+ vim.schedule_wrap(function()
+ self:fetch()
+ end)
+ )
+end
+
+function GitlabGraphQLProvider:disconnect()
+ self.timer:stop()
+ self.timer = nil
+end
+
+return GitlabGraphQLProvider
diff --git a/lua/pipeline/providers/init.lua b/lua/pipeline/providers/init.lua
new file mode 100644
index 0000000..34e4234
--- /dev/null
+++ b/lua/pipeline/providers/init.lua
@@ -0,0 +1,14 @@
+local providers = {
+ gitlab = 'gitlab.graphql',
+ github = 'github.rest',
+}
+
+---@class pipeline.Providers: { [string]: pipeline.Provider }
+---@field github pipeline.providers.github.rest.Provider
+local M = setmetatable({}, {
+ __index = function(_, key)
+ return require('pipeline.providers.' .. providers[key])
+ end,
+})
+
+return M
diff --git a/lua/pipeline/providers/provider.lua b/lua/pipeline/providers/provider.lua
new file mode 100644
index 0000000..6093200
--- /dev/null
+++ b/lua/pipeline/providers/provider.lua
@@ -0,0 +1,93 @@
+--# selene: allow(unused_variable)
+
+--- Generic class for all providers
+---
+--- We assume the following is true for most ci/cd systems:
+--- 1. There's multiple different pipelines defined
+--- - Github calls these Workflow
+--- - Gitlab calls these Pipeline
+--- - We call it Pipeline
+--- 2. Each pipeline trigger an instance of a pipeline run
+--- - Github calls these WorkflowRun
+--- - Gitlab calls these Pipeline (they don't seem to have a different
+--- terminology for the pipeline definition and a pipeline run)
+--- - We call it Run
+--- 3. Each pipeline has multiple jobs
+--- - We all call these Job
+--- 4. Each job has a name and multiple steps
+--- - Github calls these Step with an optional name
+--- - Gitlab has just a list of scripts
+--- - We call it Step
+---
+--- Optionally, some providers have a concept of stages, ex. Gitlab, to group
+--- jobs within a Pipeline. Groups can be used to sort/group these in the UI
+--- tree.
+
+---@class pipeline.Provider
+---@field protected config pipeline.Config
+---@field protected store pipeline.Store
+---@field private listener_count integer
+local Provider = {}
+
+---@return pipeline.Provider
+function Provider:extend()
+ return setmetatable({}, {
+ __index = self,
+ })
+end
+
+function Provider.detect()
+ vim.notify_once('Provider does not implement detect', vim.log.levels.WARN)
+
+ return false
+end
+
+---@generic T: pipeline.Provider
+---@param config pipeline.Config
+---@param store pipeline.Store
+---@param opts? table
+---@return self
+function Provider:new(config, store, opts)
+ local instance = setmetatable({}, {
+ __index = self,
+ })
+ instance.config = config
+ instance.store = store
+ instance.listener_count = 0
+ instance:init(opts or {})
+ return instance
+end
+
+---Constructor function, called when creating a new Provider instance.
+---@param opts table
+function Provider:init(opts) end
+
+---Start fetching or listening to data from the provider.
+function Provider:connect() end
+
+---Stop fetching or listening to data from the provider.
+function Provider:disconnect() end
+
+---Dispatch a new pipeline run
+---@param pipeline pipeline.Pipeline|nil
+function Provider:dispatch(pipeline) end
+
+---Retry a failed pipeline run/job/step
+---@param pipeline_object pipeline.PipelineObject|nil
+function Provider:retry(pipeline_object) end
+
+function Provider:listen()
+ if self.listener_count == 0 then
+ self:connect()
+ end
+ self.listener_count = self.listener_count + 1
+end
+
+function Provider:close()
+ self.listener_count = self.listener_count - 1
+ if self.listener_count == 0 then
+ self:disconnect()
+ end
+end
+
+return Provider
diff --git a/lua/pipeline/providers/types.lua b/lua/pipeline/providers/types.lua
new file mode 100644
index 0000000..0ef5391
--- /dev/null
+++ b/lua/pipeline/providers/types.lua
@@ -0,0 +1,39 @@
+---@meta
+
+---@alias pipeline.Status 'pending'|'queued'|'requested'|'waiting'|'in_progress'|'completed'|'unknown'
+---@alias pipeline.Conclusion 'success'|'failure'|'cancelled'|'skipped'|'action_required'|'unknown'
+
+---@class pipeline.BasePipelineObject
+---@field name? string
+---@field action? fun()
+---@field url? string
+---@field status? pipeline.Status
+---@field conclusion? pipeline.Conclusion
+---@field cancelable? boolean
+---@field meta? table
+---@package
+
+---A pipeline definition, in Github terms, this would be a workflow.
+---@class pipeline.Pipeline: pipeline.BasePipelineObject
+---@field kind 'pipeline'
+---@field pipeline_id string
+
+---A run of a pipeline, in Github terms, this would be a workflow run.
+---@class pipeline.Run: pipeline.BasePipelineObject
+---@field kind 'run'
+---@field run_id string
+---@field pipeline_id string
+
+---A job within a pipeline run.
+---@class pipeline.Job: pipeline.BasePipelineObject
+---@field kind 'job'
+---@field job_id string
+---@field run_id string
+
+---A step within a job.
+---@class pipeline.Step: pipeline.BasePipelineObject
+---@field kind 'step'
+---@field step_id string
+---@field job_id string
+
+---@alias pipeline.PipelineObject pipeline.Pipeline|pipeline.Run|pipeline.Job|pipeline.Step
diff --git a/lua/pipeline/store.lua b/lua/pipeline/store.lua
new file mode 100644
index 0000000..e9ed5b2
--- /dev/null
+++ b/lua/pipeline/store.lua
@@ -0,0 +1,69 @@
+local utils = require('pipeline.utils')
+
+---@class pipeline.StatePipelineConfig
+---@field last_read integer
+---@field config table
+
+---@class pipeline.State
+---@field title string
+---@field repo string
+---@field server string
+---@field pipelines pipeline.Pipeline[]
+---@field runs table Runs indexed by pipeline id
+---@field jobs table Jobs indexed by run id
+---@field steps table Steps indexed by job id
+---@field workflow_configs table
+local initialState = {
+ title = 'pipeline.nvim',
+ repo = '',
+ server = '',
+ pipelines = {},
+ latest_run = nil,
+ runs = {},
+ jobs = {},
+ steps = {},
+ workflow_configs = {},
+}
+
+---@class pipeline.Store
+---@field package _state pipeline.State
+---@field package _update fun(state: pipeline.State)[]
+local M = {
+ _state = initialState,
+ _update = {},
+}
+
+---@param state pipeline.State
+local function emit_update(state)
+ for _, update in ipairs(M._update) do
+ update(state)
+ end
+end
+
+emit_update = utils.debounced(vim.schedule_wrap(emit_update))
+
+---@param fn fun(render_state: pipeline.State): pipeline.State|nil
+function M.update_state(fn)
+ M._state = fn(M._state) or M._state
+
+ emit_update(M._state)
+end
+
+---@param fn function
+function M.on_update(fn)
+ table.insert(M._update, fn)
+end
+
+---@param fn function
+function M.off_update(fn)
+ M._update = vim.tbl_filter(function(f)
+ return f ~= fn
+ end, M._update)
+end
+
+---@return pipeline.State
+function M.get_state()
+ return M._state
+end
+
+return M
diff --git a/lua/gh-actions/ui.lua b/lua/pipeline/ui.lua
similarity index 57%
rename from lua/gh-actions/ui.lua
rename to lua/pipeline/ui.lua
index 55936d1..ebb2541 100644
--- a/lua/gh-actions/ui.lua
+++ b/lua/pipeline/ui.lua
@@ -1,11 +1,9 @@
-local Split = require('nui.split')
-local Config = require('gh-actions.config')
-local store = require('gh-actions.store')
-local Render = require('gh-actions.ui.render')
+local store = require('pipeline.store')
local M = {
---@type NuiSplit
split = nil,
+ ---@type pipeline.Render
renderer = nil,
}
@@ -13,31 +11,36 @@ local function get_cursor_line(line)
return line or vim.api.nvim_win_get_cursor(M.split.winid)[1]
end
----TODO: This should be a local function
+---@param kind pipeline.RenderLocationKind
---@param line? integer
----@return GhWorkflow|nil
-function M.get_workflow(line)
+local function get_location(kind, line)
line = get_cursor_line(line)
- return M.renderer:get_location('workflow', line)
+ return M.renderer:get_location(kind, line)
end
----TODO: This should be a local function
---@param line? integer
----@return GhWorkflowRun|nil
-function M.get_workflow_run(line)
- line = get_cursor_line(line)
+---@return pipeline.Pipeline|nil
+function M.get_pipeline(line)
+ return get_location('pipeline', line)
+end
- return M.renderer:get_location('workflow_run', line)
+---@param line? integer
+---@return pipeline.Run|nil
+function M.get_run(line)
+ return get_location('run', line)
end
----TODO: This should be a local function
---@param line? integer
----@return GhWorkflowRunJob|nil
-function M.get_workflow_job(line)
- line = get_cursor_line(line)
+---@return pipeline.Job|nil
+function M.get_job(line)
+ return get_location('job', line)
+end
- return M.renderer:get_location('workflow_job', line)
+---@param line? integer
+---@return pipeline.Step|nil
+function M.get_step(line)
+ return get_location('step', line)
end
local function is_visible()
@@ -59,6 +62,10 @@ function M.render()
end
function M.setup()
+ local Split = require('nui.split')
+ local Config = require('pipeline.config')
+ local Render = require('pipeline.ui.render')
+
M.split = Split(Config.options.split)
M.renderer = Render.new(store)
diff --git a/lua/gh-actions/ui/buffer.lua b/lua/pipeline/ui/buffer.lua
similarity index 95%
rename from lua/gh-actions/ui/buffer.lua
rename to lua/pipeline/ui/buffer.lua
index 043614c..175c968 100644
--- a/lua/gh-actions/ui/buffer.lua
+++ b/lua/pipeline/ui/buffer.lua
@@ -8,23 +8,28 @@
---@field protected _lines Line[]
---@field protected _indent number
local Buffer = {
- ns = vim.api.nvim_create_namespace('gh-actions'),
+ ns = vim.api.nvim_create_namespace('pipeline.nvim'),
}
---@param opts? { indent?: integer }
function Buffer.new(opts)
- opts = opts or {}
-
local self = setmetatable({}, {
__index = Buffer,
})
- self._lines = {}
- self._indent = opts.indent or 2
+ self:init(opts)
return self
end
+---@param opts? { indent?: integer }
+function Buffer:init(opts)
+ opts = opts or {}
+
+ self._lines = {}
+ self._indent = opts.indent or 2
+end
+
---@param line Line
---@param opts? { indent?: number | nil }
function Buffer:append_line(line, opts)
diff --git a/lua/pipeline/ui/components/input.lua b/lua/pipeline/ui/components/input.lua
new file mode 100644
index 0000000..b804777
--- /dev/null
+++ b/lua/pipeline/ui/components/input.lua
@@ -0,0 +1,22 @@
+---@param opts { prompt: string, title: string, default_value: string, on_submit: fun(value: string) }
+local function Input(opts)
+ local NuiInput = require('nui.input')
+
+ return NuiInput({
+ relative = 'editor',
+ position = '50%',
+ size = {
+ width = #opts.prompt + 32,
+ },
+ border = {
+ style = 'rounded',
+ text = { top = opts.title },
+ },
+ }, {
+ prompt = opts.prompt,
+ default_value = opts.default_value,
+ on_submit = opts.on_submit,
+ })
+end
+
+return Input
diff --git a/lua/pipeline/ui/components/select.lua b/lua/pipeline/ui/components/select.lua
new file mode 100644
index 0000000..aaece61
--- /dev/null
+++ b/lua/pipeline/ui/components/select.lua
@@ -0,0 +1,32 @@
+---@param opts { prompt: string, title: string, options: string[], on_submit: fun(value: { text: string }) }
+local function Select(opts)
+ local Menu = require('nui.menu')
+ local lines = { Menu.separator(opts.prompt) }
+
+ for _, option in ipairs(opts.options) do
+ table.insert(lines, Menu.item(option))
+ end
+
+ return Menu({
+ relative = 'editor',
+ position = '50%',
+ size = {
+ width = #opts.prompt + 32,
+ },
+ border = {
+ style = 'rounded',
+ text = { top = opts.title },
+ },
+ }, {
+ lines = lines,
+ on_submit = opts.on_submit,
+ keymap = {
+ focus_next = { 'j', '', '' },
+ focus_prev = { 'k', '', '' },
+ close = { '', '' },
+ submit = { '', '' },
+ },
+ })
+end
+
+return Select
diff --git a/lua/gh-actions/ui/render.lua b/lua/pipeline/ui/render.lua
similarity index 50%
rename from lua/gh-actions/ui/render.lua
rename to lua/pipeline/ui/render.lua
index c5865cd..22af12e 100644
--- a/lua/gh-actions/ui/render.lua
+++ b/lua/pipeline/ui/render.lua
@@ -1,23 +1,25 @@
-local Config = require('gh-actions.config')
-local Buffer = require('gh-actions.ui.buffer')
-local utils = require('gh-actions.utils')
+local Config = require('pipeline.config')
+local Buffer = require('pipeline.ui.buffer')
+local utils = require('pipeline.utils')
---TODO: Shade background like https://github.com/akinsho/toggleterm.nvim/blob/2e477f7ee8ee8229ff3158e3018a067797b9cd38/lua/toggleterm/colors.lua
----@class GhActionsRenderLocation
+---@alias pipeline.RenderLocationKind 'pipeline'|'run'|'job'|'step'
+
+---@class pipeline.RenderLocation
---@field value any
----@field kind string
+---@field kind pipeline.RenderLocationKind
---@field from? integer
---@field to? integer
----@class GhActionsRender:Buffer
----@field store { get_state: fun(): GhActionsState }
----@field locations GhActionsRenderLocation[]
-local GhActionsRender = {
+---@class pipeline.Render:Buffer
+---@field store { get_state: fun(): pipeline.State }
+---@field locations pipeline.RenderLocation[]
+local PipelineRender = {
locations = {},
}
-setmetatable(GhActionsRender, { __index = Buffer })
+setmetatable(PipelineRender, { __index = Buffer })
---@param run { status: string, conclusion: string }
---@return string
@@ -43,23 +45,24 @@ local function get_status_highlight(run, prefix)
end
if run.status == 'completed' then
- return 'GhActions'
+ return 'Pipeline'
.. utils.string.upper_first(prefix)
.. utils.string.upper_first(run.conclusion)
end
- return 'GhActions'
+ return 'Pipeline'
.. utils.string.upper_first(prefix)
.. utils.string.upper_first(run.status)
end
----@param store { get_state: fun(): GhActionsState }
----@return GhActionsRender
-function GhActionsRender.new(store)
- local self = setmetatable(Buffer.new { indent = Config.options.indent }, {
- __index = GhActionsRender,
+---@param store { get_state: fun(): pipeline.State }
+---@return pipeline.Render
+function PipelineRender.new(store)
+ local self = setmetatable({}, {
+ __index = PipelineRender,
})
- ---@cast self GhActionsRender
+
+ Buffer.init(self, { indent = Config.options.indent })
self.store = store
@@ -67,60 +70,47 @@ function GhActionsRender.new(store)
end
---@param bufnr integer
-function GhActionsRender:render(bufnr)
+function PipelineRender:render(bufnr)
self._lines = {}
self.locations = {}
local state = self.store:get_state()
self:title(state)
- self:workflows(state)
+ self:pipelines(state)
self:trim()
Buffer.render(self, bufnr)
end
--- Render title of the split window
----@param state GhActionsState
-function GhActionsRender:title(state)
- if not state.repo then
- self:append('Github Workflows'):nl():nl()
- else
- self:append(string.format('Github Workflows for %s', state.repo)):nl():nl()
- end
+---@param state pipeline.State
+function PipelineRender:title(state)
+ self:append(state.title):nl():nl()
end
---- Render each workflow
----@param state GhActionsState
-function GhActionsRender:workflows(state)
- local workflows = state.workflows
- local workflow_runs = state.workflow_runs
-
- local workflow_runs_by_workflow_id = utils.group_by(function(workflow_run)
- return workflow_run.workflow_id
- end, workflow_runs)
-
- for _, workflow in ipairs(workflows) do
- local runs = workflow_runs_by_workflow_id[workflow.id] or {}
-
- self:workflow(state, workflow, runs)
+--- Render each pipeline
+---@param state pipeline.State
+function PipelineRender:pipelines(state)
+ for _, pipeline in ipairs(state.pipelines) do
+ self:pipeline(state, pipeline, state.runs[pipeline.pipeline_id] or {})
end
end
----@param state GhActionsState
----@param workflow GhWorkflow
----@param runs GhWorkflowRun[]
-function GhActionsRender:workflow(state, workflow, runs)
- self:with_location({ kind = 'workflow', value = workflow }, function()
+---@param state pipeline.State
+---@param pipeline pipeline.Pipeline
+---@param runs pipeline.Run[]
+function PipelineRender:pipeline(state, pipeline, runs)
+ self:with_location({ kind = 'pipeline', value = pipeline }, function()
local runs_n = math.min(5, #runs)
self
:status_icon(runs[1])
:append(' ')
- :append(workflow.name, get_status_highlight(runs[1], 'run'))
+ :append(pipeline.name, get_status_highlight(runs[1], 'run'))
:append(
- state.workflow_configs[workflow.id]
- and state.workflow_configs[workflow.id].config.on.workflow_dispatch
+ state.workflow_configs[pipeline.pipeline_id]
+ and state.workflow_configs[pipeline.pipeline_id].config.on.workflow_dispatch
and (' ' .. Config.options.icons.workflow_dispatch)
or ''
)
@@ -129,7 +119,7 @@ function GhActionsRender:workflow(state, workflow, runs)
-- TODO cutting down on how many we list here, as we fetch 100 overall repo
-- runs on opening the split. I guess we do want to have this configurable.
for _, run in ipairs { unpack(runs, 1, runs_n) } do
- self:workflow_run(state, run)
+ self:run(state, run)
end
end)
@@ -138,47 +128,45 @@ function GhActionsRender:workflow(state, workflow, runs)
end
end
----@param state GhActionsState
----@param run GhWorkflowRun
-function GhActionsRender:workflow_run(state, run)
- self:with_location({ kind = 'workflow_run', value = run }, function()
+---@param state pipeline.State
+---@param run pipeline.Run
+function PipelineRender:run(state, run)
+ self:with_location({ kind = 'run', value = run }, function()
self
:status_icon(run, { indent = 1 })
:append(' ')
- :append(
- run.head_commit.message:gsub('\n.*', ''),
- get_status_highlight(run, 'run')
- )
+ :append(run.name, get_status_highlight(run, 'run'))
:nl()
if run.status ~= 'completed' then
- for _, job in ipairs(state.workflow_jobs[run.id] or {}) do
- self:workflow_job(job)
+ for _, job in ipairs(state.jobs[run.run_id] or {}) do
+ self:job(state, job)
end
end
end)
end
----@param job GhWorkflowRunJob
-function GhActionsRender:workflow_job(job)
- self:with_location({ kind = 'workflow_job', value = job }, function()
+---@param state pipeline.State
+---@param job pipeline.Job
+function PipelineRender:job(state, job)
+ self:with_location({ kind = 'job', value = job }, function()
self
:status_icon(job, { indent = 2 })
:append(' ')
:append(job.name, get_status_highlight(job, 'job'))
:nl()
- if job.status ~= 'completed' then
- for _, step in ipairs(job.steps) do
- self:workflow_step(step)
+ if job.status ~= 'completed' and state.steps[job.job_id] then
+ for _, step in ipairs(state.steps[job.job_id]) do
+ self:step(step)
end
end
end)
end
----@param step GhWorkflowRunJobStep
-function GhActionsRender:workflow_step(step)
- self:with_location({ kind = 'workflow_step', value = step }, function()
+---@param step pipeline.Step
+function PipelineRender:step(step)
+ self:with_location({ kind = 'step', value = step }, function()
self
:status_icon(step, { indent = 3 })
:append(' ')
@@ -189,7 +177,7 @@ end
---@param status { status: string, conclusion: string }
---@param opts? { indent?: number | nil }
-function GhActionsRender:status_icon(status, opts)
+function PipelineRender:status_icon(status, opts)
opts = opts or {}
self:append(
@@ -201,17 +189,17 @@ function GhActionsRender:status_icon(status, opts)
return self
end
----@param location GhActionsRenderLocation
-function GhActionsRender:append_location(location)
+---@param location pipeline.RenderLocation
+function PipelineRender:append_location(location)
table.insert(
self.locations,
vim.tbl_extend('keep', location, { to = self:get_current_line_nr() - 1 })
)
end
----@param kind string
+---@param kind pipeline.RenderLocationKind
---@param line integer
-function GhActionsRender:get_location(kind, line)
+function PipelineRender:get_location(kind, line)
for _, loc in ipairs(self.locations) do
if loc.kind == kind and line >= loc.from and line <= loc.to then
return loc.value
@@ -219,9 +207,9 @@ function GhActionsRender:get_location(kind, line)
end
end
----@param location GhActionsRenderLocation
+---@param location pipeline.RenderLocation
---@param fn fun()
-function GhActionsRender:with_location(location, fn)
+function PipelineRender:with_location(location, fn)
local start_line_nr = self:get_current_line_nr()
fn()
@@ -231,4 +219,4 @@ function GhActionsRender:with_location(location, fn)
)
end
-return GhActionsRender
+return PipelineRender
diff --git a/lua/gh-actions/utils.lua b/lua/pipeline/utils.lua
similarity index 94%
rename from lua/gh-actions/utils.lua
rename to lua/pipeline/utils.lua
index f73aa36..615e613 100644
--- a/lua/gh-actions/utils.lua
+++ b/lua/pipeline/utils.lua
@@ -1,4 +1,4 @@
-local stringUtils = require('gh-actions.utils.string')
+local stringUtils = require('pipeline.utils.string')
local M = {
string = stringUtils,
@@ -105,7 +105,7 @@ function M.open(uri)
return M.float { style = '', file = uri }
end
- local Config = require('gh-actions.config')
+ local Config = require('pipeline.config')
local cmd
if Config.options.browser then
@@ -141,7 +141,7 @@ end
function M.is_nil(value)
return value == nil
or value == vim.NIL
- or require('gh-actions.yaml').is_yaml_nil(value)
+ or require('pipeline.yaml').is_yaml_nil(value)
end
return M
diff --git a/lua/gh-actions/utils/icons.lua b/lua/pipeline/utils/icons.lua
similarity index 87%
rename from lua/gh-actions/utils/icons.lua
rename to lua/pipeline/utils/icons.lua
index 5c32eaa..57df520 100644
--- a/lua/gh-actions/utils/icons.lua
+++ b/lua/pipeline/utils/icons.lua
@@ -1,10 +1,10 @@
local function Config()
- return require('gh-actions.config')
+ return require('pipeline.config')
end
local M = {}
----@return GhActionsIcons
+---@return pipeline.config.Icons
function M.get_icons()
return Config().options.icons
end
diff --git a/lua/gh-actions/utils/string.lua b/lua/pipeline/utils/string.lua
similarity index 100%
rename from lua/gh-actions/utils/string.lua
rename to lua/pipeline/utils/string.lua
diff --git a/lua/gh-actions/yaml.lua b/lua/pipeline/yaml.lua
similarity index 65%
rename from lua/gh-actions/yaml.lua
rename to lua/pipeline/yaml.lua
index 9a6abad..0ad0eb9 100644
--- a/lua/gh-actions/yaml.lua
+++ b/lua/pipeline/yaml.lua
@@ -1,5 +1,5 @@
-local utils = require('gh-actions.utils')
-local has_rust_module, rust = pcall(require, 'gh-actions.rust')
+local utils = require('pipeline.utils')
+local has_native_module, native_yaml = pcall(require, 'pipeline_native.yaml')
local M = {}
@@ -14,8 +14,8 @@ end
---@param yamlstr string|nil
---@return table|nil
function M.parse_yaml(yamlstr)
- if has_rust_module then
- return rust.parse_yaml(yamlstr or '')
+ if has_native_module then
+ return native_yaml.parse_yaml(yamlstr or '')
else
local result = vim
.system({ 'yq', '-j' }, {
@@ -28,7 +28,7 @@ function M.parse_yaml(yamlstr)
end
function M.is_yaml_nil(value)
- return has_rust_module and value == rust.NIL
+ return has_native_module and value == native_yaml.NIL
end
return M
diff --git a/lua/pipeline_native/.gitkeep b/lua/pipeline_native/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/lua/pipeline_native/yaml.meta.lua b/lua/pipeline_native/yaml.meta.lua
new file mode 100644
index 0000000..2bd405e
--- /dev/null
+++ b/lua/pipeline_native/yaml.meta.lua
@@ -0,0 +1,13 @@
+---@meta pipeline_native.yaml
+--# selene: allow(unused_variable)
+
+local M = {}
+
+---@param yamlstr string
+---@return table|nil
+function M.parse_yaml(yamlstr) end
+
+---@type unknown
+M.NIL = nil
+
+return M
diff --git a/src/lib.rs b/src/lib.rs
index 21e3145..6a1f613 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -2,14 +2,14 @@ use mlua::prelude::{Lua, LuaError, LuaResult, LuaTable};
use mlua::LuaSerdeExt;
use serde_yaml::Value as YamlValue;
-fn yaml_to_lua(lua: &Lua, yamlstr: String) -> LuaResult> {
+fn yaml_to_lua(lua: &Lua, yamlstr: String) -> LuaResult {
let yaml: YamlValue = serde_yaml::from_str(&yamlstr).map_err(LuaError::external)?;
lua.to_value(&yaml)
}
#[mlua::lua_module]
-fn libgh_actions_rust(lua: &Lua) -> LuaResult {
+fn pipeline_native_yaml(lua: &Lua) -> LuaResult {
let exports = lua.create_table()?;
exports.set("NIL", lua.null())?;
diff --git a/tests/github_spec.lua b/tests/github_spec.lua
index 868ddb7..85d6f77 100644
--- a/tests/github_spec.lua
+++ b/tests/github_spec.lua
@@ -1,6 +1,6 @@
-local gh = require('gh-actions.github')
-
describe('get_github_token', function()
+ local gh = require('pipeline.providers.github.utils')
+
before_each(function()
vim.env.GITHUB_TOKEN = nil
end)
diff --git a/tests/init.lua b/tests/init.lua
new file mode 100644
index 0000000..646b826
--- /dev/null
+++ b/tests/init.lua
@@ -0,0 +1,28 @@
+-- mimic startup option `--clean`
+local function clean_startup()
+ for _, path in ipairs(vim.split(vim.o.runtimepath, ',')) do
+ if
+ string.find(path, vim.fn.expand('~/.config/nvim'))
+ or string.find(path, vim.fn.expand('~/.local/share/nvim/site'))
+ then
+ vim.opt.packpath:remove(path)
+ vim.opt.runtimepath:remove(path)
+ end
+ end
+end
+
+clean_startup()
+
+local root_dir = vim.fn
+ .fnamemodify(vim.trim(vim.fn.system('git rev-parse --show-toplevel')), ':p')
+ :gsub('/$', '')
+
+package.path =
+ string.format('%s;%s/?.lua;%s/?/init.lua', package.path, root_dir, root_dir)
+
+vim.opt.packpath:prepend(root_dir .. '/.tests/site')
+
+vim.cmd([[
+ packadd plenary.nvim
+ packadd nui.nvim
+]])
diff --git a/tests/minimal.vim b/tests/minimal.vim
deleted file mode 100644
index fea3608..0000000
--- a/tests/minimal.vim
+++ /dev/null
@@ -1,6 +0,0 @@
-set rtp+=.
-set rtp+=../plenary.nvim
-set rtp+=../nui.nvim
-
-runtime! plugin/plenary.vim
-runtime! plugin/nui.vim
diff --git a/tests/minimal_init.vim b/tests/minimal_init.vim
deleted file mode 120000
index 636c5dd..0000000
--- a/tests/minimal_init.vim
+++ /dev/null
@@ -1 +0,0 @@
-minimal.vim
\ No newline at end of file
diff --git a/tests/utils_spec.lua b/tests/utils_spec.lua
index 86a5832..9c67eb8 100644
--- a/tests/utils_spec.lua
+++ b/tests/utils_spec.lua
@@ -1,13 +1,16 @@
-local utils = require("gh-actions.utils")
+local utils = require('pipeline.utils')
-describe("uniq", function()
- it("it remove duplicate values from table", function()
+describe('uniq', function()
+ it('it remove duplicate values from table', function()
local list = { { id = 1 }, { id = 1 }, { id = 2 }, { id = 1 }, { id = 3 } }
local function get_id(li)
return li.id
end
- assert.are.same(utils.uniq(get_id, list), { { id = 1 }, { id = 2 }, { id = 3 } })
+ assert.are.same(
+ utils.uniq(get_id, list),
+ { { id = 1 }, { id = 2 }, { id = 3 } }
+ )
end)
end)