├── .github └── workflows │ ├── ci.yml │ ├── codeql-analysis.yml │ └── released.yml ├── .gitignore ├── .husky └── pre-push ├── .prettierrc.json ├── .yamllint.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── action.yml ├── eslint.config.mjs ├── package-lock.json ├── package.json ├── scripts ├── post_action_check.ts └── prepare-release.sh ├── src ├── config.ts ├── index.ts ├── install.ts ├── install_linux.ts ├── install_macos.ts ├── install_windows.ts ├── neovim.ts ├── shell.ts ├── system.ts ├── validate.ts └── vim.ts ├── test ├── config.ts ├── helper.ts ├── install_linux.ts ├── install_macos.ts ├── neovim.ts ├── shell.ts ├── validate.ts └── vim.ts ├── tsconfig.eslint.json └── tsconfig.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | stable-and-nightly: 6 | name: Stable and nightly 7 | strategy: 8 | matrix: 9 | os: [ubuntu-latest, macos-latest, windows-latest, macos-13, ubuntu-24.04, ubuntu-24.04-arm] 10 | version: [stable, nightly] 11 | neovim: [true, false] 12 | fail-fast: false 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: '20' 19 | cache: npm 20 | - run: npm ci 21 | - run: npm run build 22 | - uses: ./ 23 | with: 24 | version: ${{ matrix.version }} 25 | neovim: ${{ matrix.neovim }} 26 | id: vim 27 | - name: Validate action result 28 | run: node ./scripts/post_action_check.js "${{ matrix.neovim }}" "${{ matrix.version }}" "${{ steps.vim.outputs.executable }}" 29 | 30 | # Note: separate from stable-and-nightly since jobs.{id}.name.strategy.matrix.exclude seems not working 31 | # Note: This is the last version which should not run `vim.exe -silent -register` on Windows (#37) 32 | vim-v9_1_0626: 33 | name: Vim v9.1.0626 34 | strategy: 35 | matrix: 36 | os: [ubuntu-latest, macos-latest, windows-latest] 37 | runs-on: ${{ matrix.os }} 38 | steps: 39 | - uses: actions/checkout@v4 40 | - uses: actions/setup-node@v4 41 | with: 42 | node-version: '20' 43 | cache: npm 44 | - run: npm ci 45 | - run: npm run build 46 | - uses: ./ 47 | with: 48 | version: v9.1.0626 49 | configure-args: | 50 | --with-features=huge --enable-fail-if-missing --disable-nls 51 | id: vim 52 | - name: Validate action result 53 | run: node ./scripts/post_action_check.js "false" "v9.1.0626" "${{ steps.vim.outputs.executable }}" 54 | 55 | nvim-v0_4_4: 56 | name: Neovim v0.4.4 57 | strategy: 58 | matrix: 59 | os: [ubuntu-latest, macos-latest, windows-latest] 60 | runs-on: ${{ matrix.os }} 61 | steps: 62 | - uses: actions/checkout@v4 63 | - uses: actions/setup-node@v4 64 | with: 65 | node-version: '20' 66 | cache: npm 67 | - run: npm ci 68 | - run: npm run build 69 | - uses: ./ 70 | with: 71 | neovim: true 72 | version: v0.4.4 73 | id: neovim 74 | - name: Validate action result 75 | run: node ./scripts/post_action_check.js "true" "v0.4.4" "${{ steps.neovim.outputs.executable }}" 76 | 77 | nvim-v0_10_3: 78 | name: Neovim v0.10.3 79 | runs-on: ubuntu-latest 80 | steps: 81 | - uses: actions/checkout@v4 82 | - uses: actions/setup-node@v4 83 | with: 84 | node-version: '20' 85 | cache: npm 86 | - run: npm ci 87 | - run: npm run build 88 | - uses: ./ 89 | with: 90 | neovim: true 91 | version: v0.10.3 92 | id: neovim 93 | - name: Validate action result 94 | run: node ./scripts/post_action_check.js "true" "v0.10.3" "${{ steps.neovim.outputs.executable }}" 95 | 96 | test-and-lint: 97 | name: Check unit tests and lints 98 | strategy: 99 | matrix: 100 | # macos-latest for tests on arm64 101 | os: [ubuntu-latest, macos-latest] 102 | runs-on: ${{ matrix.os }} 103 | steps: 104 | - uses: actions/checkout@v4 105 | - uses: actions/setup-node@v4 106 | with: 107 | node-version: '20' 108 | cache: npm 109 | - run: npm ci 110 | - name: Run unit tests 111 | run: npm test 112 | env: 113 | GITHUB_TOKEN: ${{ github.token }} 114 | - run: npm run lint 115 | - uses: actions/setup-python@v5 116 | with: 117 | python-version: '3.x' 118 | - run: pip install yamllint 119 | - run: yamllint --strict .github/workflows 120 | - name: Check workflow files 121 | run: | 122 | bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) 123 | ./actionlint -color 124 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | # The branches below must be a subset of the branches above 9 | branches: 10 | - master 11 | schedule: 12 | - cron: '31 9 * * 1' 13 | workflow_dispatch: 14 | 15 | jobs: 16 | analyze: 17 | name: Analyze 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions/setup-node@v4 22 | with: 23 | node-version: "20" 24 | - uses: github/codeql-action/init@v3 25 | with: 26 | languages: javascript-typescript 27 | - uses: github/codeql-action/analyze@v3 28 | with: 29 | category: "/language:javascript-typescript" 30 | -------------------------------------------------------------------------------- /.github/workflows/released.yml: -------------------------------------------------------------------------------- 1 | name: Post-release check 2 | on: 3 | schedule: 4 | - cron: '0 0 * * 0' 5 | push: 6 | paths: 7 | - 'CHANGELOG.md' 8 | - '.github/workflows/released.yml' 9 | workflow_dispatch: 10 | 11 | jobs: 12 | validate: 13 | name: Validate release 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, macos-latest, windows-latest, macos-13, ubuntu-24.04-arm] 17 | neovim: [true, false] 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - uses: rhysd/action-setup-vim@v1 21 | id: vim 22 | with: 23 | neovim: ${{ matrix.neovim }} 24 | version: stable 25 | - uses: actions/checkout@v4 26 | - uses: actions/setup-node@v4 27 | with: 28 | node-version: '20' 29 | - run: npm ci 30 | - run: npm run build 31 | - name: Validate action result 32 | run: node ./scripts/post_action_check.js "${{ matrix.neovim }}" "stable" "${{ steps.vim.outputs.executable }}" 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /src/*.js 3 | /test/*.js 4 | /scripts/*.js 5 | *.js.map 6 | /env.sh 7 | /.nyc_output 8 | /coverage 9 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | npx concurrently -c auto npm:lint npm:test 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "semi": true, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "printWidth": 120, 7 | "arrowParens": "avoid" 8 | } 9 | -------------------------------------------------------------------------------- /.yamllint.yml: -------------------------------------------------------------------------------- 1 | extends: default 2 | 3 | rules: 4 | line-length: disable 5 | document-start: disable 6 | truthy: disable 7 | braces: 8 | min-spaces-inside: 1 9 | max-spaces-inside: 1 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # [v1.4.2](https://github.com/rhysd/action-setup-vim/releases/tag/v1.4.2) - 2025-03-28 3 | 4 | - Fix the version of stable Neovim or Vim may be outdated on macOS by updating formulae before running `brew install`. By this fix, the new version of Neovim which was released 2 days ago is now correctly installed. ([#49](https://github.com/rhysd/action-setup-vim/issues/49)) 5 | - Add a warning message with useful information when executing `./configure` fails to build older versions of Vim. 6 | - Update dependencies including some security fixes in `@octokit/*` packages. 7 | 8 | 9 | [Changes][v1.4.2] 10 | 11 | 12 | 13 | # [v1.4.1](https://github.com/rhysd/action-setup-vim/releases/tag/v1.4.1) - 2025-02-01 14 | 15 | - Fix arm32 Linux (self-hosted runner) is rejected on checking the CPU architecture before installation. 16 | - Add ['Supported platforms' table](https://github.com/rhysd/action-setup-vim?tab=readme-ov-file#supported-platforms) to the readme document to easily know which platforms are supported for Vim/Neovim. 17 | 18 | [Changes][v1.4.1] 19 | 20 | 21 | 22 | # [v1.4.0](https://github.com/rhysd/action-setup-vim/releases/tag/v1.4.0) - 2025-02-01 23 | 24 | - Support for [Linux arm64 hosted runners](https://github.blog/changelog/2025-01-16-linux-arm64-hosted-runners-now-available-for-free-in-public-repositories-public-preview/). ([#39](https://github.com/rhysd/action-setup-vim/issues/39)) 25 | - For Neovim, Linux arm64 is supported since v0.10.4. v0.10.3 or earlier versions are not supported because of no prebuilt Linux arm64 binaries for the versions. 26 | - Fix installing Neovim after the v0.10.4 release. The installation was broken because the asset file name has been changed. ([#42](https://github.com/rhysd/action-setup-vim/issues/42), [#43](https://github.com/rhysd/action-setup-vim/issues/43), thanks [@falcucci](https://github.com/falcucci) and [@danarnold](https://github.com/danarnold) for making the patches at [#40](https://github.com/rhysd/action-setup-vim/issues/40) and [#41](https://github.com/rhysd/action-setup-vim/issues/41) respectively) 27 | 28 | [Changes][v1.4.0] 29 | 30 | 31 | 32 | # [v1.3.5](https://github.com/rhysd/action-setup-vim/releases/tag/v1.3.5) - 2024-07-28 33 | 34 | - Fix `vim` command hangs on Windows after Vim 9.1.0631. ([#37](https://github.com/rhysd/action-setup-vim/issues/37)) 35 | - Shout out to [@k-takata](https://github.com/k-takata) to say thank you for the great help at [vim/vim#15372](https://github.com/vim/vim/issues/15372). 36 | - Update the dependencies to the latest. This includes small security fixes. 37 | 38 | [Changes][v1.3.5] 39 | 40 | 41 | 42 | # [v1.3.4](https://github.com/rhysd/action-setup-vim/releases/tag/v1.3.4) - 2024-05-17 43 | 44 | - Support [Neovim v0.10](https://github.com/neovim/neovim/releases/tag/v0.10.0) new asset file names for macOS. ([#30](https://github.com/rhysd/action-setup-vim/issues/30)) 45 | - Until v0.9.5, Neovim provided a single universal executable. From v0.10.0, Neovim now provides separate two executables for arm64 and x86_64. action-setup-vim downloads a proper asset file looking at the current system's architecture. 46 | 47 | [Changes][v1.3.4] 48 | 49 | 50 | 51 | # [v1.3.3](https://github.com/rhysd/action-setup-vim/releases/tag/v1.3.3) - 2024-05-07 52 | 53 | - Remove the support for Ubuntu 18.04, which was removed from GitHub-hosted runners more than one year ago. 54 | - Improve adding `bin` directory to the `$PATH` environment variable by using `core.addPath` rather than modifying the environment variable directly. ([#33](https://github.com/rhysd/action-setup-vim/issues/33), thanks [@ObserverOfTime](https://github.com/ObserverOfTime)) 55 | - Update dependencies including some security patches. 56 | 57 | [Changes][v1.3.3] 58 | 59 | 60 | 61 | # [v1.3.2](https://github.com/rhysd/action-setup-vim/releases/tag/v1.3.2) - 2024-03-29 62 | 63 | - Fix the nightly Neovim installation was broken due to [neovim/neovim#28000](https://github.com/neovim/neovim/pull/28000). ([#30](https://github.com/rhysd/action-setup-vim/issues/30), thanks [@linrongbin16](https://github.com/linrongbin16)) 64 | - Neovim now provides `neovim-macos-arm64.tar.gz` (for Apple Silicon) and `neovim-macos-x86_64.tar.gz` (for Intel Mac) separately rather than the single `neovim-macos.tar.gz`. This change will be applied to the next stable version. 65 | - Update npm dependencies to the latest. This update includes some small security fixes. 66 | - Fix an incorrect OS version was reported in debug message on Ubuntu. 67 | 68 | [Changes][v1.3.2] 69 | 70 | 71 | 72 | # [v1.3.1](https://github.com/rhysd/action-setup-vim/releases/tag/v1.3.1) - 2024-01-31 73 | 74 | - Support [the new M1 Mac runner](https://github.blog/changelog/2024-01-30-github-actions-introducing-the-new-m1-macos-runner-available-to-open-source/) ([#28](https://github.com/rhysd/action-setup-vim/issues/28)) 75 | - On M1 Mac, Homebrew installation directory was changed from `/usr/local` to `/opt/homebrew` 76 | 77 | [Changes][v1.3.1] 78 | 79 | 80 | 81 | # [v1.3.0](https://github.com/rhysd/action-setup-vim/releases/tag/v1.3.0) - 2023-10-15 82 | 83 | - `configure-args` input was added to customize build configurations on building Vim from source. This input is useful to change `./configure` arguments to enable/disable some features of Vim. For example, when you're facing some issue on generating translation files (this sometimes happens when building older Vim), disabling the native language support would be able to avoid the issue. ([#27](https://github.com/rhysd/action-setup-vim/issues/27)) 84 | ```yaml 85 | - uses: rhysd/action-setup-vim@v1 86 | with: 87 | version: 8.0.0000 88 | configure-args: | 89 | --with-features=huge --enable-fail-if-missing --disable-nls 90 | ``` 91 | - Update the action runtime to `node20`. Now this action is run with Node.js v20. 92 | - Update all dependencies to the latest including `@actions/github` v6.0.0 and some security fixes. 93 | 94 | [Changes][v1.3.0] 95 | 96 | 97 | 98 | # [v1.2.15](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.15) - 2023-03-06 99 | 100 | - Show less output on unarchiving downloaded assets with `unzip -q` to reduce amount of logs. When [debugging is enabled](https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/enabling-debug-logging), `-q` is not added and `unzip` shows all retrieved file paths for debugging. ([#25](https://github.com/rhysd/action-setup-vim/issues/25)) 101 | - Upgrade the lock file version from v2 to v3, which largely reduces size of `package-lock.json`. 102 | - Update dependencies. 103 | 104 | [Changes][v1.2.15] 105 | 106 | 107 | 108 | # [v1.2.14](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.14) - 2023-01-09 109 | 110 | - Improve warning message when trying to build Vim older than 8.2.1119 on `macos-latest` or `macos-12` runner since the build would fail. `macos-11` runner should be used instead. 111 | - Vim older than 8.2.1119 can be built with Xcode 11 or earlier only. `macos-12` runner does not include Xcode 11 by default. And now `macos-latest` label points to `macos-12` runner. So building Vim 8.2.1119 or older on `macos-latest` would fail. 112 | - Update dependencies to fix deprecation warning from `uuid` package 113 | 114 | [Changes][v1.2.14] 115 | 116 | 117 | 118 | # [v1.2.13](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.13) - 2022-10-13 119 | 120 | - Update `@actions/core` to v1.10.0 to follow the change that [GitHub deprecated `set-output` command](https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/) recently. 121 | - Update other dependencies including `@actions/github` v5.1.1 122 | 123 | [Changes][v1.2.13] 124 | 125 | 126 | 127 | # [v1.2.12](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.12) - 2022-07-21 128 | 129 | - Fix the Neovim asset directory name for macOS has been changed from `nvim-osx64` to `nvim-macos` at Neovim v0.7.1. (thanks [@notomo](https://github.com/notomo), [#22](https://github.com/rhysd/action-setup-vim/issues/22)) 130 | - Update dependencies including `@actions/core` v1.9.0 and `@actions/github` v5.0.3. 131 | 132 | [Changes][v1.2.12] 133 | 134 | 135 | 136 | # [v1.2.11](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.11) - 2022-04-15 137 | 138 | - Fix installing `stable` or `v0.7.0` Neovim on Windows runner. The asset directory name was changed from 'Neovim' to 'nvim-win64' at v0.7.0 and the change broke this action. 139 | 140 | [Changes][v1.2.11] 141 | 142 | 143 | 144 | # [v1.2.10](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.10) - 2022-03-23 145 | 146 | - Fix installing nightly Neovim on Windows. (thanks [@notomo](https://github.com/notomo), [#20](https://github.com/rhysd/action-setup-vim/issues/20) [#21](https://github.com/rhysd/action-setup-vim/issues/21)) 147 | - Update dependencies to the latest. (including new `@actions/exec` and `@actions/io`) 148 | 149 | [Changes][v1.2.10] 150 | 151 | 152 | 153 | # [v1.2.9](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.9) - 2022-02-05 154 | 155 | - Use `node16` runner to run this action. 156 | - Update dependencies. Now TypeScript source compiles to ES2021 code since Node.js v16 supports all ES2021 features. 157 | 158 | [Changes][v1.2.9] 159 | 160 | 161 | 162 | # [v1.2.8](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.8) - 2021-10-02 163 | 164 | - Installing Neovim nightly now fallbacks to building from source when downloading assets failed (thanks [@glacambre](https://github.com/glacambre), [#18](https://github.com/rhysd/action-setup-vim/issues/18), [#9](https://github.com/rhysd/action-setup-vim/issues/9)) 165 | - This fallback logic is currently only for Linux and macOS 166 | - This fallback happens when [the release workflow](https://github.com/neovim/neovim/actions/workflows/release.yml) of [neovim/neovim](https://github.com/neovim/neovim) failed to update [the nightly release page](https://github.com/neovim/neovim/tree/nightly) 167 | - Update many dependencies including all `@actions/*` packages and TypeScript compiler 168 | - Now multiple versions of Vim/Neovim can be installed within the same job. Previously, Vim/Neovim installed via release archives or built from source were installed in `~/vim`/`~/nvim`. It meant that trying to install multiple versions caused a directory name conflict. Now they are installed in `~/vim-{ver}`/`~/nvim-{ver}` (e.g. `~/vim-v8.2.1234`, `~/nvim-nightly`) so that the conflict no longer happens. 169 | 170 | [Changes][v1.2.8] 171 | 172 | 173 | 174 | # [v1.2.7](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.7) - 2021-02-05 175 | 176 | - Fix: Installing stable Vim on `ubuntu-20.04` worker. `vim-gnome` was removed at Ubuntu 19.10. In the case, this action installs `vim-gtk3` instead. The worker is now used for `ubuntu-latest` also. ([#11](https://github.com/rhysd/action-setup-vim/issues/11)) 177 | - Improve: Better error message on an invalid value for `version` input 178 | - Improve: Update dependencies 179 | 180 | [Changes][v1.2.7] 181 | 182 | 183 | 184 | # [v1.2.6](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.6) - 2020-11-15 185 | 186 | - Fix: Build failed on building Vim older than v8.2.1119 on macOS worker. Now Vim before v8.2.1119 is built with Xcode11 since it cannot be built with Xcode12. ([#10](https://github.com/rhysd/action-setup-vim/issues/10)) 187 | - Improve: Update dependencies 188 | 189 | [Changes][v1.2.6] 190 | 191 | 192 | 193 | # [v1.2.5](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.5) - 2020-10-02 194 | 195 | - Fix: Update `@actions/core` for security patch 196 | - Improve: Internal refactoring 197 | - Improve: Update dependencies 198 | 199 | [Changes][v1.2.5] 200 | 201 | 202 | 203 | # [v1.2.4](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.4) - 2020-09-08 204 | 205 | - Improve: When an asset for stable Neovim in `stable` release is not found, fallback to the latest version release by detecting the latest version via GitHub API. API token will be given via `token` input. You don't need to set it because it is set automatically. ([#5](https://github.com/rhysd/action-setup-vim/issues/5)) 206 | - Improve: Update dependencies to the latest 207 | 208 | [Changes][v1.2.4] 209 | 210 | 211 | 212 | # [v1.2.3](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.3) - 2020-03-29 213 | 214 | - Fix: Run `apt update` before `apt install` on installing stable Vim on Linux. `apt install vim-gnome` caused an error without this 215 | 216 | [Changes][v1.2.3] 217 | 218 | 219 | 220 | # [v1.2.2](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.2) - 2020-02-22 221 | 222 | - Improve: Better error message when no asset is found on installing Neovim 223 | 224 | [Changes][v1.2.2] 225 | 226 | 227 | 228 | # [v1.2.1](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.1) - 2020-02-15 229 | 230 | - Improve: Validate the executable file before getting `--version` output 231 | 232 | [Changes][v1.2.1] 233 | 234 | 235 | 236 | # [v1.2.0](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.0) - 2020-02-02 237 | 238 | - Improve: `github-token` input was removed since it is no longer necessary. This is not a breaking change since `github-token` input is now simply ignored. 239 | - GitHub API token was used only for getting the latest release of vim-win32-installer repository on Windows. But now the latest release is detected from redirect URL. 240 | 241 | [Changes][v1.2.0] 242 | 243 | 244 | 245 | # [v1.1.3](https://github.com/rhysd/action-setup-vim/releases/tag/v1.1.3) - 2020-01-31 246 | 247 | - Fix: `version` input check was not correct for Vim 7.x (e.g. `7.4.100`, `7.4`). [Thanks @itchyny!](https://github.com/rhysd/action-setup-vim/pull/1) 248 | - Fix: Path separator was not correct on Windows 249 | - Improve: Better post-action validation on CI and internal refactoring 250 | 251 | [Changes][v1.1.3] 252 | 253 | 254 | 255 | # [v1.1.2](https://github.com/rhysd/action-setup-vim/releases/tag/v1.1.2) - 2020-01-31 256 | 257 | - Fix: GitHub API call may fail relying on IP address of the worker (ref: [actions/setup-go#16](https://github.com/actions/setup-go/issues/16)) 258 | 259 | [Changes][v1.1.2] 260 | 261 | 262 | 263 | # [v1.1.1](https://github.com/rhysd/action-setup-vim/releases/tag/v1.1.1) - 2020-01-31 264 | 265 | - Improve: `github-token` input is now optional even if you install Vim on Windows worker 266 | - Improve: Update dev-dependencies 267 | 268 | [Changes][v1.1.1] 269 | 270 | 271 | 272 | # [v1.1.0](https://github.com/rhysd/action-setup-vim/releases/tag/v1.1.0) - 2020-01-29 273 | 274 | - New: Specific version tag can be set to `version` input like `version: v8.2.0126`. Please read [documentation](https://github.com/rhysd/action-setup-vim#readme) for more details. 275 | 276 | [Changes][v1.1.0] 277 | 278 | 279 | 280 | # [v1.0.2](https://github.com/rhysd/action-setup-vim/releases/tag/v1.0.2) - 2020-01-28 281 | 282 | - Improve: Now all input environment variables (starting with `INPUT_`) are filtered on executing subprocesses ([actions/toolkit#309](https://github.com/actions/toolkit/issues/309)) 283 | - Improve: Unit tests were added for validation of inputs and outputs 284 | - Improve: Better validation error messages 285 | - Improve: Better descriptions in README.md 286 | 287 | [Changes][v1.0.2] 288 | 289 | 290 | 291 | # [v1.0.1](https://github.com/rhysd/action-setup-vim/releases/tag/v1.0.1) - 2020-01-25 292 | 293 | - Improve: Install stable Neovim with Homebrew on macOS. Now it is installed via `brew install neovim` 294 | 295 | [Changes][v1.0.1] 296 | 297 | 298 | 299 | # [v1.0.0](https://github.com/rhysd/action-setup-vim/releases/tag/v1.0.0) - 2020-01-24 300 | 301 | First release :tada: 302 | 303 | Please read [README.md](https://github.com/rhysd/action-setup-vim#readme) for usage. 304 | 305 | [Changes][v1.0.0] 306 | 307 | 308 | [v1.4.2]: https://github.com/rhysd/action-setup-vim/compare/v1.4.1...v1.4.2 309 | [v1.4.1]: https://github.com/rhysd/action-setup-vim/compare/v1.4.0...v1.4.1 310 | [v1.4.0]: https://github.com/rhysd/action-setup-vim/compare/v1.3.5...v1.4.0 311 | [v1.3.5]: https://github.com/rhysd/action-setup-vim/compare/v1.3.4...v1.3.5 312 | [v1.3.4]: https://github.com/rhysd/action-setup-vim/compare/v1.3.3...v1.3.4 313 | [v1.3.3]: https://github.com/rhysd/action-setup-vim/compare/v1.3.2...v1.3.3 314 | [v1.3.2]: https://github.com/rhysd/action-setup-vim/compare/v1.3.1...v1.3.2 315 | [v1.3.1]: https://github.com/rhysd/action-setup-vim/compare/v1.3.0...v1.3.1 316 | [v1.3.0]: https://github.com/rhysd/action-setup-vim/compare/v1.2.15...v1.3.0 317 | [v1.2.15]: https://github.com/rhysd/action-setup-vim/compare/v1.2.14...v1.2.15 318 | [v1.2.14]: https://github.com/rhysd/action-setup-vim/compare/v1.2.13...v1.2.14 319 | [v1.2.13]: https://github.com/rhysd/action-setup-vim/compare/v1.2.12...v1.2.13 320 | [v1.2.12]: https://github.com/rhysd/action-setup-vim/compare/v1.2.11...v1.2.12 321 | [v1.2.11]: https://github.com/rhysd/action-setup-vim/compare/v1.2.10...v1.2.11 322 | [v1.2.10]: https://github.com/rhysd/action-setup-vim/compare/v1.2.9...v1.2.10 323 | [v1.2.9]: https://github.com/rhysd/action-setup-vim/compare/v1.2.8...v1.2.9 324 | [v1.2.8]: https://github.com/rhysd/action-setup-vim/compare/v1.2.7...v1.2.8 325 | [v1.2.7]: https://github.com/rhysd/action-setup-vim/compare/v1.2.6...v1.2.7 326 | [v1.2.6]: https://github.com/rhysd/action-setup-vim/compare/v1.2.5...v1.2.6 327 | [v1.2.5]: https://github.com/rhysd/action-setup-vim/compare/v1.2.4...v1.2.5 328 | [v1.2.4]: https://github.com/rhysd/action-setup-vim/compare/v1.2.3...v1.2.4 329 | [v1.2.3]: https://github.com/rhysd/action-setup-vim/compare/v1.2.2...v1.2.3 330 | [v1.2.2]: https://github.com/rhysd/action-setup-vim/compare/v1.2.1...v1.2.2 331 | [v1.2.1]: https://github.com/rhysd/action-setup-vim/compare/v1.2.0...v1.2.1 332 | [v1.2.0]: https://github.com/rhysd/action-setup-vim/compare/v1.1.3...v1.2.0 333 | [v1.1.3]: https://github.com/rhysd/action-setup-vim/compare/v1.1.2...v1.1.3 334 | [v1.1.2]: https://github.com/rhysd/action-setup-vim/compare/v1.1.1...v1.1.2 335 | [v1.1.1]: https://github.com/rhysd/action-setup-vim/compare/v1.1.0...v1.1.1 336 | [v1.1.0]: https://github.com/rhysd/action-setup-vim/compare/v1.0.2...v1.1.0 337 | [v1.0.2]: https://github.com/rhysd/action-setup-vim/compare/v1.0.1...v1.0.2 338 | [v1.0.1]: https://github.com/rhysd/action-setup-vim/compare/v1.0.0...v1.0.1 339 | [v1.0.0]: https://github.com/rhysd/action-setup-vim/tree/v1.0.0 340 | 341 | 342 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to action-setup-vim 2 | ================================ 3 | 4 | Thank you for contributing to [action-setup-vim][repo]. This document is for development. 5 | 6 | ## Testing 7 | 8 | For testing validation for inputs and outputs, run unit tests: 9 | 10 | ```sh 11 | npm run test 12 | ``` 13 | 14 | Tests for installation logic are done in [CI workflows][ci] in E2E testing manner. All combinations 15 | of inputs are tested on the workflows triggered by `push` and `pull_request` events. 16 | 17 | After building and running `action-setup-vim` action, the workflow verifies the post conditions 18 | with [post_action_check.ts](./scripts/post_action_check.ts). 19 | 20 | ## Linting 21 | 22 | In addition to type checking with TypeScript compiler, the following command checks the sources with 23 | [eslint][] and [pretteir][]. 24 | 25 | ```sh 26 | npm run lint 27 | ``` 28 | 29 | ## Node.js version 30 | 31 | Node.js version must be aligned with Node.js runtime in GitHub Actions. Check the version at 32 | `runs.using` in [action.yml](./action.yml) and use the same version for development. 33 | 34 | ## How to create a new release 35 | 36 | When releasing v1.2.3: 37 | 38 | 1. Make sure that `node --version` shows Node.js v20. 39 | 2. Run `$ bash scripts/prepare-release.sh v1.2.3`. It builds everything and prunes `node_modules` 40 | for removing all dev-dependencies. Then it copies built artifacts to `dev/v1` branch and makes 41 | a new commit and tag `v1.2.3`. Finally it rearrange `v1` and `v1.2` tags to point the new commit. 42 | 3. Check changes in the created commit with `git show`. 43 | 4. If ok, run `$ bash ./prepare-release.sh v1.2.3 --done` to apply the release to the remote. The 44 | script will push the branch and the new tag, then force-push the existing tags. 45 | 46 | ## Post release check 47 | 48 | [Post-release check workflow][post-release] runs to check released `rhysd/action-setup-vim@v1` action. 49 | The workflow runs when modifying `CHANGELOG.md` and also runs on every Sunday 00:00 UTC. 50 | 51 | [repo]: https://github.com/rhysd/action-setup-vim 52 | [ci]: https://github.com/rhysd/action-setup-vim/actions/workflows/ci.yml 53 | [eslint]: https://eslint.org/ 54 | [prettier]: https://prettier.io/ 55 | [post-release]: https://github.com/rhysd/action-setup-vim/actions?query=workflow%3A%22Post-release+check%22+branch%3Amaster 56 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | the MIT License 2 | 3 | Copyright (c) 2020 rhysd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 16 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 17 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR 20 | THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GitHub Action to setup Vim and Neovim 2 | ===================================== 3 | [![Build status][ci-badge]][ci] 4 | [![Action Marketplace][release-badge]][marketplace] 5 | 6 | [action-setup-vim][proj] is an action for [GitHub Actions][github-actions] to setup [Vim][vim] or 7 | [Neovim][neovim] on Linux, macOS and Windows. Stable releases, nightly releases and specifying 8 | versions are supported. 9 | 10 | For stable releases, this action will install Vim or Neovim from system's package manager or 11 | official releases since it is the most popular way to install them and it's faster than building 12 | from source. 13 | 14 | For nightly release, this action basically installs the nightly release of Vim or Neovim from 15 | official releases. If unavailable, it builds executables from sources. 16 | 17 | For more details, please read the following 'Installation details' section. 18 | 19 | ## Why? 20 | 21 | Since preparing Vim editor is highly depending on a platform. On Linux, Vim is usually installed via 22 | system's package manager like `apt`. On macOS, MacVim is the most popular Vim distribution and 23 | usually installed via Homebrew. On Windows, [official installers][win-inst] are provided. 24 | 25 | Neovim provides releases [on GitHub][neovim-release] and system package managers. 26 | 27 | If you're an author of Vim and/or Neovim plugin and your plugin has some tests, you'd like to run 28 | them across platforms on Vim and/or Neovim. action-setup-vim will help the installation with only 29 | one step. You don't need to separate workflow jobs for each platforms and Vim/Neovim. 30 | 31 | ## Usage 32 | 33 | Install the latest stable Vim: 34 | 35 | ```yaml 36 | - uses: rhysd/action-setup-vim@v1 37 | ``` 38 | 39 | Install the latest nightly Vim: 40 | 41 | ```yaml 42 | - uses: rhysd/action-setup-vim@v1 43 | with: 44 | version: nightly 45 | ``` 46 | 47 | Install the latest Vim v8.1.123. The version is a tag name in [vim/vim][vim] repository. Please see 48 | the following 'Choosing a specific version' section as well: 49 | 50 | ```yaml 51 | - uses: rhysd/action-setup-vim@v1 52 | with: 53 | version: v8.1.0123 54 | ``` 55 | 56 | When you want to customize the build configuration for Vim, `configure-args` input is available. 57 | The input is passed to `./configure` option when building Vim from source: 58 | 59 | ```yaml 60 | - uses: rhysd/action-setup-vim@v1 61 | with: 62 | version: nightly 63 | configure-args: | 64 | --with-features=huge --enable-fail-if-missing --disable-nls 65 | ``` 66 | 67 | Install the latest stable Neovim: 68 | 69 | ```yaml 70 | - uses: rhysd/action-setup-vim@v1 71 | with: 72 | neovim: true 73 | ``` 74 | 75 | Install the latest nightly Neovim: 76 | 77 | ```yaml 78 | - uses: rhysd/action-setup-vim@v1 79 | with: 80 | neovim: true 81 | version: nightly 82 | ``` 83 | 84 | Install the Neovim v0.4.3. Please see the following 'Choosing a specific version' section as well: 85 | 86 | ```yaml 87 | - uses: rhysd/action-setup-vim@v1 88 | with: 89 | neovim: true 90 | version: v0.4.3 91 | ``` 92 | 93 | After the setup, `vim` executable will be available for Vim and `nvim` executable will be available 94 | for Neovim. 95 | 96 | Real-world examples are workflows in [clever-f.vim][clever-f-workflow] and 97 | [git-messenger.vim][git-messenger-workflow]. And you can see [this repository's CI workflows][ci]. 98 | They run this action with all combinations of the inputs. 99 | 100 | For comprehensive lists of inputs and outputs, please refer [action.yml](./action.yml). 101 | 102 | ## Outputs 103 | 104 | This action sets installed executable path to the action's `executable` output. You can use it for 105 | running Vim command in the steps later. 106 | 107 | Here is an example to set Vim executable to run unit tests with [themis.vim][vim-themis]. 108 | 109 | ```yaml 110 | - uses: actions/checkout@v2 111 | with: 112 | repository: thinca/vim-themis 113 | path: vim-themis 114 | - uses: rhysd/action-setup-vim@v1 115 | id: vim 116 | - name: Run unit tests with themis.vim 117 | env: 118 | THEMIS_VIM: ${{ steps.vim.outputs.executable }} 119 | run: | 120 | ./vim-themis/bin/themis ./test 121 | ``` 122 | 123 | ## Supported platforms 124 | 125 | | | Vim | Neovim | 126 | |---------------------------|-----------------------------|-------------------------| 127 | | Linux x86_64 | :white_check_mark: | :white_check_mark: | 128 | | Linux arm64 | :white_check_mark: | :warning: since v0.10.4 | 129 | | Linux arm32 (self-hosted) | :white_check_mark: | :x: | 130 | | Windows x86_64 | :warning: no stable version | :white_check_mark: | 131 | | macOS x86_64 | :white_check_mark: | :white_check_mark: | 132 | | macOS arm64 | :white_check_mark: | :white_check_mark: | 133 | 134 | - :white_check_mark: : Supported 135 | - :warning: : Supported with limitation 136 | - :x: : Unsupported 137 | 138 | ## Installation details 139 | 140 | ### Vim 141 | 142 | `vX.Y.Z` represents a specific version such as `v8.2.0126`. 143 | 144 | | OS | Version | Installation | 145 | |---------|-----------|-----------------------------------------------------------------------------| 146 | | Linux | `stable` | Install [`vim-gtk3`][vim-gtk3] via `apt` package manager | 147 | | Linux | `nightly` | Build the HEAD of [vim/vim][vim] repository | 148 | | Linux | `vX.Y.Z` | Build the `vX.Y.Z` tag of [vim/vim][vim] repository | 149 | | macOS | `stable` | Install MacVim via `brew install macvim` | 150 | | macOS | `nightly` | Build the HEAD of [vim/vim][vim] repository | 151 | | macOS | `vX.Y.Z` | Build the `vX.Y.Z` tag of [vim/vim][vim] repository | 152 | | Windows | `stable` | There is no stable release for Windows so fall back to `nightly` | 153 | | Windows | `nightly` | Install the latest release from [the installer repository][win-inst] | 154 | | Windows | `vX.Y.Z` | Install the release at `vX.Y.Z` tag of [the installer repository][win-inst] | 155 | 156 | For stable releases on all platforms and nightly on Windows, `gvim` executable is also available. 157 | 158 | When installing without system's package manager, Vim is installed at `$HOME/vim`. 159 | 160 | **Note:** When you build Vim older than 8.2.1119 on macOS, Xcode 11 or earlier is necessary due to 161 | lack of [this patch][vim_8_2_1119]. Please try `macos-11` runner instead of the latest macOS runner 162 | in the case. 163 | 164 | ### Neovim 165 | 166 | `vX.Y.Z` represents a specific version such as `v0.4.3`. 167 | 168 | | OS | Version | Installation | 169 | |---------|-----------|---------------------------------------------------------------------------| 170 | | Linux | `stable` | Install from the latest [Neovim stable release][nvim-stable] | 171 | | Linux | `nightly` | Install from the latest [Neovim nightly release][nvim-nightly] | 172 | | Linux | `vX.Y.Z` | Install the release at `vX.Y.Z` tag of [neovim/neovim][neovim] repository | 173 | | macOS | `stable` | `brew install neovim` using Homebrew | 174 | | macOS | `nightly` | Install from the latest [Neovim nightly release][nvim-nightly] | 175 | | macOS | `vX.Y.Z` | Install the release at `vX.Y.Z` tag of [neovim/neovim][neovim] repository | 176 | | Windows | `stable` | Install from the latest [Neovim stable release][nvim-stable] | 177 | | Windows | `nightly` | Install from the latest [Neovim nightly release][nvim-nightly] | 178 | | Windows | `vX.Y.Z` | Install the release at `vX.Y.Z` tag of [neovim/neovim][neovim] repository | 179 | 180 | Only on Windows, `nvim-qt.exe` executable is available for GUI. 181 | 182 | When installing without system's package manager, Neovim is installed at `$HOME/nvim`. 183 | 184 | **Note:** Ubuntu 18.04 supports official [`neovim` package][ubuntu-nvim] but this action does not 185 | install it. As of now, GitHub Actions also supports Ubuntu 16.04. 186 | 187 | **Note:** When downloading a Neovim asset from [`stable` release][nvim-stable] on GitHub, the asset 188 | is rarely missing in the release. In the case, this action will get the latest version tag from 189 | GitHub API and use it instead of `stable` tag (see [#5][issue-5] for more details). 190 | 191 | **Note:** When downloading a Neovim asset from [`nightly` release][nvim-nightly] on GitHub, it might 192 | cause 'Asset Not Found' error. This is because the Nightly build failed due to some reason in 193 | [neovim/neovim][neovim] CI workflow. In the case, this action tries to build Neovim from sources on 194 | Linux and macOS workers. It gives up installation on other platforms. 195 | 196 | ## Choosing a specific version 197 | 198 | ### Vim 199 | 200 | If Vim is built from source, any tag version should be available. 201 | 202 | If Vim is installed via release asset (on Windows), please check 203 | [vim-win32-installer releases page][win-inst-release] to know which versions are available. 204 | The repository makes a release once per day (nightly). 205 | 206 | Note that Vim's patch number in version tags is in 4-digits like `v8.2.0126`. Omitting leading 207 | zeros such as `v8.2.126` or `v8.2.1` is not allowed. 208 | 209 | ### Neovim 210 | 211 | When installing the specific version of Neovim, this action downloads release assets from 212 | [neovim/neovim][neovim]. Please check [neovim/neovim releases page][neovim-release] to know which 213 | versions have release assets. For example, 214 | [Neovim 0.4.0](https://github.com/neovim/neovim/releases/tag/v0.4.0) has no Windows releases so it 215 | is not available for installing Neovim on Windows. 216 | 217 | ## Current limitation 218 | 219 | - GUI version (gVim and nvim-qt) is supported partially as described in above section. 220 | - Installing Vim/Neovim from system's package manager is not configurable. For example, arguments 221 | cannot be passed to `brew install`. 222 | 223 | These are basically not a technical limitation. Please let me know by creating an issue if you want 224 | some of them. 225 | 226 | ## License 227 | 228 | Distributed under [the MIT license](./LICENSE.txt). 229 | 230 | [ci-badge]: https://github.com/rhysd/action-setup-vim/actions/workflows/ci.yml/badge.svg 231 | [ci]: https://github.com/rhysd/action-setup-vim/actions/workflows/ci.yml 232 | [release-badge]: https://img.shields.io/github/v/release/rhysd/action-setup-vim.svg 233 | [marketplace]: https://github.com/marketplace/actions/setup-vim 234 | [proj]: https://github.com/rhysd/action-setup-vim 235 | [github-actions]: https://github.com/features/actions 236 | [vim]: https://github.com/vim/vim 237 | [neovim]: https://github.com/neovim/neovim 238 | [win-inst]: https://github.com/vim/vim-win32-installer 239 | [nvim-stable]: https://github.com/neovim/neovim/releases/tag/stable 240 | [nvim-nightly]: https://github.com/neovim/neovim/releases/tag/nightly 241 | [clever-f-workflow]: https://github.com/rhysd/clever-f.vim/blob/master/.github/workflows/ci.yml 242 | [git-messenger-workflow]: https://github.com/rhysd/git-messenger.vim/blob/master/.github/workflows/ci.yml 243 | [vim-gtk3]: https://packages.ubuntu.com/search?keywords=vim-gtk3 244 | [ubuntu-nvim]: https://packages.ubuntu.com/search?keywords=neovim 245 | [vim-themis]: https://github.com/thinca/vim-themis 246 | [win-inst-release]: https://github.com/vim/vim-win32-installer/releases 247 | [neovim-release]: https://github.com/neovim/neovim/releases 248 | [generate-pat]: https://github.com/settings/tokens/new 249 | [gh-action-secrets]: https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets 250 | [issue-5]: https://github.com/rhysd/action-setup-vim/issues/5 251 | [vim_8_2_1119]: https://github.com/vim/vim/commit/5289783e0b07cfc3f92ee933261ca4c4acdca007 252 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Setup Vim' 2 | author: 'rhysd ' 3 | description: 'Setup Vim or Neovim text editors on GitHub Actions' 4 | branding: 5 | icon: 'edit' 6 | color: 'green' 7 | 8 | inputs: 9 | version: 10 | description: > 11 | Version of Vim or Neovim to install. Valid values are 'stable', 'nightly' or version tag such 12 | as 'v8.2.0126'. Note that this value must exactly match to a tag name when installing the 13 | specific version. 14 | required: false 15 | default: 'stable' 16 | neovim: 17 | description: > 18 | Setting to true will install Neovim. 19 | required: false 20 | default: false 21 | configure-args: 22 | description: > 23 | Arguments passed to ./configure execution when building Vim from source. 24 | required: false 25 | token: 26 | description: > 27 | Personal access token for GitHub API. It is used for calling GitHub API when Neovim asset is 28 | not found in stable releases and needs to fallback. You don't need to set this input since it 29 | is set automatically. 30 | default: ${{ github.token }} 31 | 32 | outputs: 33 | executable: 34 | description: > 35 | Absolute file path to the installed executable. 36 | 37 | runs: 38 | using: 'node20' 39 | main: 'src/index.js' 40 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from '@eslint/js'; 4 | import ts from 'typescript-eslint'; 5 | import mocha from 'eslint-plugin-mocha'; 6 | import n from 'eslint-plugin-n'; 7 | 8 | export default ts.config( 9 | eslint.configs.recommended, 10 | ...ts.configs.recommendedTypeChecked, 11 | n.configs['flat/recommended'], 12 | { 13 | languageOptions: { 14 | parserOptions: { 15 | projectService: true, 16 | project: 'tsconfig.json', 17 | }, 18 | }, 19 | }, 20 | { 21 | rules: { 22 | 'prefer-spread': 'off', 23 | '@typescript-eslint/explicit-member-accessibility': 'off', 24 | 'n/no-missing-import': 'off', 25 | eqeqeq: 'error', 26 | '@typescript-eslint/explicit-function-return-type': 'error', 27 | '@typescript-eslint/no-floating-promises': 'error', 28 | '@typescript-eslint/no-unnecessary-type-arguments': 'error', 29 | '@typescript-eslint/no-non-null-assertion': 'error', 30 | '@typescript-eslint/no-empty-interface': 'error', 31 | '@typescript-eslint/restrict-plus-operands': 'error', 32 | '@typescript-eslint/no-extra-non-null-assertion': 'error', 33 | '@typescript-eslint/prefer-nullish-coalescing': 'error', 34 | '@typescript-eslint/prefer-optional-chain': 'error', 35 | '@typescript-eslint/prefer-includes': 'error', 36 | '@typescript-eslint/prefer-for-of': 'error', 37 | '@typescript-eslint/prefer-string-starts-ends-with': 'error', 38 | '@typescript-eslint/prefer-readonly': 'error', 39 | '@typescript-eslint/prefer-ts-expect-error': 'error', 40 | '@typescript-eslint/no-non-null-asserted-optional-chain': 'error', 41 | '@typescript-eslint/await-thenable': 'error', 42 | '@typescript-eslint/no-unnecessary-boolean-literal-compare': 'error', 43 | '@typescript-eslint/ban-ts-comment': [ 44 | 'error', 45 | { 46 | 'ts-ignore': true, 47 | 'ts-nocheck': true, 48 | }, 49 | ], 50 | '@typescript-eslint/naming-convention': [ 51 | 'error', 52 | { 53 | selector: 'default', 54 | format: ['camelCase', 'PascalCase', 'UPPER_CASE'], 55 | leadingUnderscore: 'allow', 56 | }, 57 | ], 58 | 'no-unused-vars': 'off', 59 | '@typescript-eslint/no-unused-vars': 'error', 60 | '@typescript-eslint/no-confusing-void-expression': 'error', 61 | '@typescript-eslint/non-nullable-type-assertion-style': 'error', 62 | 'no-return-await': 'off', 63 | '@typescript-eslint/return-await': ['error', 'in-try-catch'], 64 | '@typescript-eslint/no-invalid-void-type': 'error', 65 | '@typescript-eslint/prefer-as-const': 'error', 66 | '@typescript-eslint/consistent-indexed-object-style': 'error', 67 | '@typescript-eslint/no-base-to-string': 'error', 68 | '@typescript-eslint/switch-exhaustiveness-check': ['error', { considerDefaultExhaustiveForUnions: true }], 69 | 'n/handle-callback-err': 'error', 70 | 'n/prefer-promises/fs': 'error', 71 | 'n/prefer-global/buffer': ['error', 'never'], 72 | 'n/prefer-global/process': ['error', 'never'], 73 | 'n/prefer-node-protocol': 'error', 74 | 'n/no-sync': 'error', 75 | }, 76 | }, 77 | { 78 | files: ['scripts/*.ts'], 79 | rules: { 80 | 'n/no-sync': 'off', 81 | }, 82 | }, 83 | { 84 | files: ['test/*.ts'], 85 | // The cast is workaround for https://github.com/lo1tuma/eslint-plugin-mocha/issues/392 86 | .../** @type {{recommended: import('eslint').Linter.Config}} */ (mocha.configs).recommended, 87 | }, 88 | { 89 | files: ['test/*.ts'], 90 | rules: { 91 | '@typescript-eslint/no-unsafe-return': 'off', 92 | '@typescript-eslint/restrict-template-expressions': 'off', 93 | '@typescript-eslint/no-unsafe-member-access': 'off', 94 | '@typescript-eslint/no-unsafe-assignment': 'off', 95 | '@typescript-eslint/no-explicit-any': 'off', 96 | '@typescript-eslint/no-unsafe-call': 'off', 97 | '@typescript-eslint/no-require-imports': 'off', 98 | '@typescript-eslint/naming-convention': 'off', 99 | 'mocha/no-setup-in-describe': 'off', 100 | 'mocha/no-hooks-for-single-case': 'off', 101 | 'mocha/max-top-level-suites': 'off', 102 | 'mocha/consistent-spacing-between-blocks': 'off', // Conflict with prettier 103 | 'mocha/no-exclusive-tests': 'error', 104 | 'mocha/no-pending-tests': 'error', 105 | 'mocha/no-top-level-hooks': 'error', 106 | 'mocha/consistent-interface': ['error', { interface: 'BDD' }], 107 | }, 108 | }, 109 | { 110 | files: ['eslint.config.mjs'], 111 | languageOptions: { 112 | parserOptions: { 113 | projectService: false, 114 | project: 'tsconfig.eslint.json', 115 | }, 116 | }, 117 | rules: { 118 | '@typescript-eslint/naming-convention': 'off', 119 | 'n/no-extraneous-import': 'off', 120 | }, 121 | }, 122 | ); 123 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "action-setup-vim", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "GitHub Actions action for installing Vim/Neovim", 6 | "engines": { 7 | "node": ">=20.0.0" 8 | }, 9 | "type": "module", 10 | "main": "src/index.js", 11 | "scripts": { 12 | "build": "tsc -p .", 13 | "watch:tsc": "tsc -p . --watch --preserveWatchOutput --pretty", 14 | "watch:mocha": "mocha --watch-files \"./test/*.js\"", 15 | "watch": "concurrently -c auto npm:watch:tsc npm:watch:mocha", 16 | "lint:eslint": "eslint --max-warnings 0 \"./**/*.ts\" eslint.config.mjs", 17 | "lint:tsc-eslint": "tsc -p tsconfig.eslint.json --pretty", 18 | "lint:prettier": "prettier --check \"./**/*.ts\" \"./**/*.mjs\"", 19 | "lint": "concurrently -c auto npm:lint:eslint npm:lint:prettier npm:lint:tsc-eslint", 20 | "fix:eslint": "eslint --fix \"./**/*.ts\"", 21 | "fix:prettier": "prettier --write \"./**/*.ts\" \"./**/*.mjs\"", 22 | "fix": "concurrently -m 1 -c auto npm:fix:eslint npm:fix:prettier", 23 | "mocha": "mocha ./test", 24 | "test": "concurrently -m 1 -c auto npm:build npm:mocha", 25 | "nyc": "nyc --reporter=lcov --reporter=text-summary npm run mocha && (which open && open ./coverage/lcov-report/index.html || true)", 26 | "cov": "concurrently -m 1 -c auto npm:build npm:nyc", 27 | "prepare": "husky" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/rhysd/action-setup-vim.git" 32 | }, 33 | "keywords": [ 34 | "github", 35 | "action", 36 | "vim", 37 | "neovim", 38 | "text editor" 39 | ], 40 | "author": "rhysd ", 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/rhysd/action-setup-vim/issues" 44 | }, 45 | "homepage": "https://github.com/rhysd/action-setup-vim#readme", 46 | "dependencies": { 47 | "@actions/core": "^1.11.1", 48 | "@actions/exec": "^1.1.1", 49 | "@actions/github": "^6.0.1", 50 | "@actions/io": "^1.1.3", 51 | "node-fetch": "^3.3.2", 52 | "shlex": "^2.1.2" 53 | }, 54 | "devDependencies": { 55 | "@types/eslint": "^9.6.1", 56 | "@types/mocha": "^10.0.10", 57 | "@types/node": "^22.15.29", 58 | "concurrently": "^9.1.2", 59 | "eslint": "^9.28.0", 60 | "eslint-plugin-mocha": "^11.1.0", 61 | "eslint-plugin-n": "^17.18.0", 62 | "esmock": "^2.7.0", 63 | "husky": "^9.1.7", 64 | "mocha": "^11.5.0", 65 | "nyc": "^17.1.0", 66 | "prettier": "^3.5.3", 67 | "typescript": "^5.8.3", 68 | "typescript-eslint": "^8.33.0" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /scripts/post_action_check.ts: -------------------------------------------------------------------------------- 1 | import { homedir } from 'node:os'; 2 | import * as path from 'node:path'; 3 | import { strict as assert } from 'node:assert'; 4 | import { spawnSync } from 'node:child_process'; 5 | import { existsSync } from 'node:fs'; 6 | import process from 'node:process'; 7 | 8 | function log(...args: unknown[]): void { 9 | console.log('[post_action_check]:', ...args); 10 | } 11 | 12 | function ok(x: unknown, m?: string): asserts x { 13 | assert.ok(x, m); 14 | } 15 | 16 | interface Args { 17 | neovim: boolean; 18 | version: string; 19 | output: string; 20 | } 21 | 22 | function parseArgs(args: string[]): Args { 23 | if (args.length !== 5) { 24 | throw new Error('3 arguments must be set: `node ./scripts/post_action_check.js {neovim?} {version} {output}`'); 25 | } 26 | 27 | const neovim = args[2].toLowerCase() === 'true'; 28 | 29 | return { neovim, version: args[3], output: args[4] }; 30 | } 31 | 32 | function expectedExecutable(neovim: boolean, ver: string): string { 33 | if (neovim) { 34 | switch (process.platform) { 35 | case 'darwin': 36 | if (ver === 'stable') { 37 | if (process.arch === 'arm64') { 38 | return '/opt/homebrew/bin/nvim'; 39 | } else { 40 | return '/usr/local/bin/nvim'; 41 | } 42 | } else { 43 | // nightly or specific version 44 | return path.join(homedir(), `nvim-${ver}/bin/nvim`); 45 | } 46 | case 'linux': 47 | return path.join(homedir(), `nvim-${ver}/bin/nvim`); 48 | case 'win32': 49 | return path.join(homedir(), `nvim-${ver}/bin/nvim.exe`); 50 | default: 51 | break; 52 | } 53 | } else { 54 | // vim 55 | switch (process.platform) { 56 | case 'darwin': 57 | if (ver === 'stable') { 58 | if (process.arch === 'arm64') { 59 | return '/opt/homebrew/bin/vim'; 60 | } else { 61 | return '/usr/local/bin/vim'; 62 | } 63 | } else { 64 | // nightly or specific version 65 | return path.join(homedir(), `vim-${ver}/bin/vim`); 66 | } 67 | case 'linux': 68 | if (ver === 'stable') { 69 | return '/usr/bin/vim'; 70 | } else { 71 | // nightly or specific version 72 | return path.join(homedir(), `vim-${ver}/bin/vim`); 73 | } 74 | case 'win32': 75 | return path.join(homedir(), `vim-${ver}/vim.exe`); 76 | default: 77 | break; 78 | } 79 | } 80 | throw new Error(`Unexpected platform '${process.platform}'`); 81 | } 82 | 83 | function main(): void { 84 | log('Running with argv:', process.argv); 85 | 86 | const args = parseArgs(process.argv); 87 | log('Command line arguments:', args); 88 | 89 | const exe = expectedExecutable(args.neovim, args.version); 90 | log('Validating output. Expected executable:', exe); 91 | assert.equal(exe, args.output); 92 | assert.ok(existsSync(exe)); 93 | 94 | const bin = path.dirname(exe); 95 | log(`Validating '${bin}' is in $PATH`); 96 | ok(process.env['PATH']); 97 | const pathSep = process.platform === 'win32' ? ';' : ':'; 98 | const paths = process.env['PATH'].split(pathSep); 99 | ok(paths.includes(bin), `'${bin}' is not included in '${process.env['PATH']}'`); 100 | 101 | log('Validating executable'); 102 | const proc = spawnSync(exe, ['-N', '-c', 'quit'], { timeout: 5000 }); 103 | let stderr = proc.stderr.toString(); 104 | assert.equal(proc.error, undefined); 105 | assert.equal(proc.status, 0, `stderr: ${stderr}`); 106 | assert.equal(proc.signal, null, `stderr: ${stderr}`); 107 | 108 | log('Validating version'); 109 | const ver = spawnSync(exe, ['--version'], { timeout: 5000 }); 110 | stderr = ver.stderr.toString(); 111 | assert.equal(ver.error, undefined); 112 | assert.equal(ver.status, 0, `stderr: ${stderr}`); 113 | assert.equal(ver.signal, null, `stderr: ${stderr}`); 114 | const stdout = ver.stdout.toString(); 115 | 116 | if (args.version !== 'stable' && args.version !== 'nightly') { 117 | if (args.neovim) { 118 | const l = `NVIM ${args.version}`; 119 | ok(stdout.includes(l), `First line '${l}' should be included in stdout: ${stdout}`); 120 | } else { 121 | const m = args.version.match(/^v(\d+\.\d+)\.(\d+)$/); 122 | ok(m); 123 | const major = m[1]; 124 | const patch = parseInt(m[2], 10); 125 | 126 | const l = `VIM - Vi IMproved ${major}`; 127 | ok(stdout.includes(l), `First line '${l}' should be included in stdout: ${stdout}`); 128 | 129 | // assert.match is not available since it is experimental 130 | ok( 131 | stdout.includes(`Included patches: 1-${patch}`), 132 | `Patch 1-${patch} should be included in stdout: ${stdout}`, 133 | ); 134 | } 135 | } else { 136 | const editorName = args.neovim ? 'NVIM' : 'VIM - Vi IMproved'; 137 | ok(stdout.includes(editorName), `Editor name '${editorName}' should be included in stdout: ${stdout}`); 138 | } 139 | 140 | log('OK'); 141 | } 142 | 143 | main(); 144 | -------------------------------------------------------------------------------- /scripts/prepare-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Arguments check 6 | if [[ "$#" != 1 ]] && [[ "$#" != 2 ]] || [[ "$1" == '--help' ]]; then 7 | echo 'Usage: prepare-release.sh {release-version} [--done]' >&2 8 | echo '' >&2 9 | echo " Release version must be in format 'v{major}.{minor}.{patch}'." >&2 10 | echo ' After making changes, add --done option and run this script again. It will' >&2 11 | echo ' push generated tags to remote for release.' >&2 12 | echo ' Note that --done must be the second argument.' >&2 13 | echo '' >&2 14 | exit 1 15 | fi 16 | 17 | version="$1" 18 | if [[ ! "$version" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 19 | echo 'Version string in the first argument must match to ''v{major}.{minor}.{patch}'' like v1.2.3' >&2 20 | exit 1 21 | fi 22 | 23 | if [[ "$#" == 2 ]] && [[ "$2" != "--done" ]]; then 24 | echo '--done option must be the second argument' >&2 25 | exit 1 26 | fi 27 | 28 | minor_version="${version%.*}" 29 | major_version="${minor_version%.*}" 30 | target_branch="dev/${major_version}" 31 | 32 | # Pre-flight check 33 | if [ ! -d .git ]; then 34 | echo 'This script must be run at root directory of this repository' >&2 35 | exit 1 36 | fi 37 | 38 | if ! git diff --quiet; then 39 | echo 'Working tree is dirty! Please ensure all changes are committed and working tree is clean' >&2 40 | exit 1 41 | fi 42 | 43 | if ! git diff --cached --quiet; then 44 | echo 'Git index is dirty! Please ensure all changes are committed and Git index is clean' >&2 45 | exit 1 46 | fi 47 | 48 | current_branch="$(git symbolic-ref --short HEAD)" 49 | 50 | # Deploy release branch 51 | if [[ "$#" == 2 ]] && [[ "$2" == "--done" ]]; then 52 | echo "Deploying ${target_branch} branch and ${version}, ${minor_version}, ${major_version} tags to 'origin' remote" 53 | if [[ "$current_branch" != "${target_branch}" ]]; then 54 | echo "--done must be run in target branch '${target_branch}' but actually run in '${current_branch}'" >&2 55 | exit 1 56 | fi 57 | 58 | set -x 59 | git push origin "${target_branch}" 60 | git push origin "${version}" 61 | git push origin "${minor_version}" --force 62 | git push origin "${major_version}" --force 63 | # Remove copied prepare-release.sh in target branch 64 | rm -rf ./prepare-release.sh 65 | set +x 66 | 67 | echo "Done. Releases were pushed to 'origin' remote" 68 | exit 0 69 | fi 70 | 71 | if [[ "$current_branch" != "master" ]]; then 72 | echo 'Current branch is not master. Please move to master before running this script' >&2 73 | exit 1 74 | fi 75 | 76 | echo "Checking tests and eslint results" 77 | 78 | npm run test 79 | npm run lint 80 | 81 | echo "Releasing to ${target_branch} branch for ${version}... (minor=${minor_version}, major=${major_version})" 82 | 83 | set -x 84 | npm install 85 | npm run build 86 | npm test 87 | npm prune --production 88 | 89 | # Remove all type definitions from node_modules since @octokit/rest/index.d.ts is very big (1.3MB) 90 | find ./node_modules/ -name '*.d.ts' -exec rm '{}' \; 91 | 92 | # Remove coverage files 93 | rm -rf ./node_modules/.cache ./.nyc_output ./coverage 94 | 95 | rm -rf .release 96 | mkdir -p .release 97 | 98 | cp action.yml src/*.js package.json package-lock.json ./scripts/prepare-release.sh .release/ 99 | cp -R node_modules .release/node_modules 100 | 101 | sha="$(git rev-parse HEAD)" 102 | 103 | git checkout "${target_branch}" 104 | git pull 105 | if [ -d node_modules ]; then 106 | git rm -rf node_modules || true 107 | rm -rf node_modules # remove node_modules/.cache 108 | fi 109 | mkdir -p src 110 | 111 | mv .release/action.yml . 112 | mv .release/*.js ./src/ 113 | mv .release/*.json . 114 | mv .release/node_modules . 115 | # Copy release script to release branch for --done 116 | mv .release/prepare-release.sh . 117 | 118 | git add action.yml ./src/*.js package.json package-lock.json node_modules 119 | git commit -m "Release ${version} at ${sha}" 120 | 121 | git tag -d "$major_version" || true 122 | git tag "$major_version" 123 | git tag -d "$minor_version" || true 124 | git tag "$minor_version" 125 | git tag "$version" 126 | set +x 127 | 128 | echo "Done. Please check 'git show' to verify changes. If ok, run this script with '--done' option like './prepare-release.sh vX.Y.Z --done'" 129 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { getInput } from '@actions/core'; 2 | import { type Os, getOs, type Arch, getArch } from './system.js'; 3 | 4 | export interface Config { 5 | readonly version: string; 6 | readonly neovim: boolean; 7 | readonly os: Os; 8 | readonly arch: Arch; 9 | readonly token: string | null; 10 | readonly configureArgs: string | null; 11 | } 12 | 13 | function getBoolean(input: string, def: boolean): boolean { 14 | const i = getInput(input).toLowerCase(); 15 | switch (i) { 16 | case '': 17 | return def; 18 | case 'true': 19 | return true; 20 | case 'false': 21 | return false; 22 | default: 23 | throw new Error(`'${input}' input only accepts boolean values 'true' or 'false' but got '${i}'`); 24 | } 25 | } 26 | 27 | function getVersion(neovim: boolean): string { 28 | const v = getInput('version'); 29 | if (v === '') { 30 | return 'stable'; 31 | } 32 | 33 | const l = v.toLowerCase(); 34 | if (l === 'stable' || l === 'nightly') { 35 | return l; 36 | } 37 | 38 | const re = neovim ? /^v\d+\.\d+\.\d+$/ : /^v7\.\d+(?:\.\d+)?$|^v\d+\.\d+\.\d{4}$/; 39 | if (!re.test(v)) { 40 | const repo = neovim ? 'neovim/neovim' : 'vim/vim'; 41 | let msg = `'version' input '${v}' is not a format of Git tags in ${repo} repository. It should match to regex /${re}/. NOTE: It requires 'v' prefix`; 42 | if (!neovim) { 43 | msg += ". And the patch version of Vim must be in 4-digits like 'v8.2.0126'"; 44 | } 45 | throw new Error(msg); 46 | } 47 | 48 | return v; 49 | } 50 | 51 | function getNeovim(): boolean { 52 | return getBoolean('neovim', false); 53 | } 54 | 55 | export function loadConfigFromInputs(): Config { 56 | const neovim = getNeovim(); 57 | return { 58 | version: getVersion(neovim), 59 | neovim, 60 | os: getOs(), 61 | arch: getArch(), 62 | configureArgs: getInput('configure-args') || null, 63 | token: getInput('token') || null, 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'node:path'; 2 | import * as core from '@actions/core'; 3 | import { loadConfigFromInputs } from './config.js'; 4 | import { install } from './install.js'; 5 | import { validateInstallation } from './validate.js'; 6 | 7 | async function main(): Promise { 8 | const config = loadConfigFromInputs(); 9 | console.log('Extracted configuration:', config); 10 | 11 | const installed = await install(config); 12 | await validateInstallation(installed); 13 | 14 | core.addPath(installed.binDir); 15 | core.debug(`'${installed.binDir}' was added to $PATH`); 16 | 17 | const fullPath = join(installed.binDir, installed.executable); 18 | core.setOutput('executable', fullPath); 19 | console.log('Installed executable:', fullPath); 20 | console.log('Installation successfully done:', installed); 21 | } 22 | 23 | main().catch((e: Error) => { 24 | if (e.stack) { 25 | core.debug(e.stack); 26 | } 27 | core.error(e.message); 28 | core.setFailed(e.message); 29 | }); 30 | -------------------------------------------------------------------------------- /src/install.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import type { Config } from './config.js'; 3 | import { install as installOnLinux } from './install_linux.js'; 4 | import { install as installOnMacOs } from './install_macos.js'; 5 | import { install as installOnWindows } from './install_windows.js'; 6 | 7 | export type ExeName = 'vim' | 'nvim' | 'vim.exe' | 'nvim.exe'; 8 | 9 | export interface Installed { 10 | readonly executable: ExeName; 11 | readonly binDir: string; 12 | } 13 | 14 | export function install(config: Config): Promise { 15 | core.debug(`Detected operating system: ${config.os}`); 16 | switch (config.os) { 17 | case 'linux': 18 | return installOnLinux(config); 19 | case 'macos': 20 | return installOnMacOs(config); 21 | case 'windows': 22 | return installOnWindows(config); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/install_linux.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import type { Installed } from './install.js'; 3 | import type { Config } from './config.js'; 4 | import { exec } from './shell.js'; 5 | import { buildVim } from './vim.js'; 6 | import { buildNightlyNeovim, downloadNeovim, downloadStableNeovim } from './neovim.js'; 7 | 8 | async function installVimStable(): Promise { 9 | core.debug('Installing stable Vim on Linux using apt'); 10 | await exec('sudo', ['apt-get', 'update', '-y', '-q']); 11 | await exec('sudo', ['apt-get', 'install', '-y', '--no-install-recommends', '-q', 'vim-gtk3']); 12 | return { 13 | executable: 'vim', 14 | binDir: '/usr/bin', 15 | }; 16 | } 17 | 18 | export async function install(config: Config): Promise { 19 | core.debug(`Installing ${config.neovim ? 'Neovim' : 'Vim'} version '${config.version}' on Linux`); 20 | if (config.neovim) { 21 | switch (config.version) { 22 | case 'stable': 23 | return downloadStableNeovim('linux', config.arch, config.token); 24 | case 'nightly': 25 | try { 26 | return await downloadNeovim(config.version, 'linux', config.arch); // await is necessary to catch error 27 | } catch (e) { 28 | const message = e instanceof Error ? e.message : String(e); 29 | core.warning( 30 | `Neovim download failure for nightly on Linux: ${message}. Falling back to installing Neovim by building it from source`, 31 | ); 32 | return buildNightlyNeovim('linux'); 33 | } 34 | default: 35 | return downloadNeovim(config.version, 'linux', config.arch); 36 | } 37 | } else { 38 | if (config.version === 'stable') { 39 | return installVimStable(); 40 | } else { 41 | return buildVim(config.version, config.os, config.configureArgs); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/install_macos.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import type { Installed } from './install.js'; 3 | import type { Config } from './config.js'; 4 | import type { Arch } from './system.js'; 5 | import { exec } from './shell.js'; 6 | import { buildVim } from './vim.js'; 7 | import { buildNightlyNeovim, downloadNeovim } from './neovim.js'; 8 | 9 | function homebrewBinDir(arch: Arch): string { 10 | switch (arch) { 11 | case 'arm64': 12 | return '/opt/homebrew/bin'; 13 | case 'x86_64': 14 | return '/usr/local/bin'; 15 | default: 16 | throw new Error(`CPU arch ${arch} is not supported by Homebrew`); 17 | } 18 | } 19 | 20 | async function brewInstall(pkg: string): Promise { 21 | await exec('brew', ['update', '--quiet']); 22 | await exec('brew', ['install', pkg, '--quiet']); 23 | } 24 | 25 | async function installVimStable(arch: Arch): Promise { 26 | core.debug('Installing stable Vim on macOS using Homebrew'); 27 | await brewInstall('macvim'); 28 | return { 29 | executable: 'vim', 30 | binDir: homebrewBinDir(arch), 31 | }; 32 | } 33 | 34 | async function installNeovimStable(arch: Arch): Promise { 35 | core.debug('Installing stable Neovim on macOS using Homebrew'); 36 | await brewInstall('neovim'); 37 | return { 38 | executable: 'nvim', 39 | binDir: homebrewBinDir(arch), 40 | }; 41 | } 42 | 43 | export async function install(config: Config): Promise { 44 | core.debug(`Installing ${config.neovim ? 'Neovim' : 'Vim'} ${config.version} version on macOS`); 45 | if (config.neovim) { 46 | switch (config.version) { 47 | case 'stable': 48 | return installNeovimStable(config.arch); 49 | case 'nightly': 50 | try { 51 | return await downloadNeovim(config.version, 'macos', config.arch); // await is necessary to catch error 52 | } catch (e) { 53 | const message = e instanceof Error ? e.message : String(e); 54 | core.warning( 55 | `Neovim download failure for nightly on macOS: ${message}. Falling back to installing Neovim by building it from source`, 56 | ); 57 | return buildNightlyNeovim('macos'); 58 | } 59 | default: 60 | return downloadNeovim(config.version, 'macos', config.arch); 61 | } 62 | } else { 63 | if (config.version === 'stable') { 64 | return installVimStable(config.arch); 65 | } else { 66 | return buildVim(config.version, config.os, config.configureArgs); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/install_windows.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import type { Installed } from './install.js'; 3 | import type { Config } from './config.js'; 4 | import { installNightlyVimOnWindows, installVimOnWindows } from './vim.js'; 5 | import { downloadNeovim, downloadStableNeovim } from './neovim.js'; 6 | 7 | export function install(config: Config): Promise { 8 | core.debug(`Installing ${config.neovim ? 'Neovim' : 'Vim'} ${config.version} version on Windows`); 9 | if (config.neovim) { 10 | switch (config.version) { 11 | case 'stable': 12 | return downloadStableNeovim('windows', config.arch, config.token); 13 | default: 14 | return downloadNeovim(config.version, 'windows', config.arch); 15 | } 16 | } else { 17 | switch (config.version) { 18 | case 'stable': 19 | core.debug('Installing stable Vim on Windows'); 20 | core.warning('No stable Vim release is officially provided for Windows. Installing nightly instead'); 21 | return installNightlyVimOnWindows('stable'); 22 | case 'nightly': 23 | return installNightlyVimOnWindows('nightly'); 24 | default: 25 | return installVimOnWindows(config.version, config.version); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/neovim.ts: -------------------------------------------------------------------------------- 1 | import { homedir } from 'node:os'; 2 | import * as path from 'node:path'; 3 | import { promises as fs } from 'node:fs'; 4 | import { Buffer } from 'node:buffer'; 5 | import fetch from 'node-fetch'; 6 | import * as core from '@actions/core'; 7 | import * as io from '@actions/io'; 8 | import * as github from '@actions/github'; 9 | import { makeTmpdir, type Os, type Arch, ensureError } from './system.js'; 10 | import { exec, unzip } from './shell.js'; 11 | import type { Installed, ExeName } from './install.js'; 12 | 13 | function exeName(os: Os): ExeName { 14 | return os === 'windows' ? 'nvim.exe' : 'nvim'; 15 | } 16 | 17 | interface Version { 18 | minor: number; 19 | patch: number; 20 | } 21 | 22 | function parseVersion(v: string): Version | null { 23 | const m = v.match(/^v0\.(\d+)\.(\d+)$/); 24 | if (m === null) { 25 | return null; 26 | } 27 | 28 | return { 29 | minor: parseInt(m[1], 10), 30 | patch: parseInt(m[2], 10), 31 | }; 32 | } 33 | 34 | export function assetFileName(version: string, os: Os, arch: Arch): string { 35 | switch (os) { 36 | case 'macos': { 37 | const v = parseVersion(version); 38 | if (v !== null && v.minor < 10) { 39 | return 'nvim-macos.tar.gz'; 40 | } 41 | switch (arch) { 42 | case 'arm64': 43 | return 'nvim-macos-arm64.tar.gz'; 44 | case 'x86_64': 45 | return 'nvim-macos-x86_64.tar.gz'; 46 | default: 47 | throw Error(`Unsupported CPU architecture for Neovim ${version} on ${os}: ${arch}`); // Should be unreachable 48 | } 49 | } 50 | case 'linux': { 51 | return assetDirName(version, os, arch) + '.tar.gz'; 52 | } 53 | case 'windows': 54 | return 'nvim-win64.zip'; 55 | } 56 | } 57 | 58 | export function assetDirName(version: string, os: Os, arch: Arch): string { 59 | switch (os) { 60 | case 'macos': { 61 | const v = parseVersion(version); 62 | if (v !== null) { 63 | // Until v0.7.0 release, 'nvim-osx64' was the asset directory name on macOS. However it was changed to 64 | // 'nvim-macos' from v0.7.1: https://github.com/neovim/neovim/pull/19029 65 | if (v.minor < 7 || (v.minor === 7 && v.patch < 1)) { 66 | return 'nvim-osx64'; 67 | } 68 | // Until v0.9.5, the single asset nvim-macos.tar.gz is released. From v0.10.0, Neovim provides 69 | // nvim-macos-arm64.tar.gz (for Apple Silicon) and nvim-macos-x86_64.tar.gz (for Intel Mac). (#30) 70 | if (v.minor < 10) { 71 | return 'nvim-macos'; 72 | } 73 | } 74 | switch (arch) { 75 | case 'arm64': 76 | return 'nvim-macos-arm64'; 77 | case 'x86_64': 78 | return 'nvim-macos-x86_64'; 79 | default: 80 | throw Error(`Unsupported CPU architecture for Neovim ${version} on ${os}: ${arch}`); // Should be unreachable 81 | } 82 | } 83 | case 'linux': { 84 | const v = parseVersion(version); 85 | if (v !== null && (v.minor < 10 || (v.minor === 10 && v.patch < 4))) { 86 | switch (arch) { 87 | case 'arm64': 88 | throw Error( 89 | `Linux arm64 has been only supported since Neovim v0.10.4 but the requested version is ${version}`, 90 | ); 91 | case 'x86_64': 92 | return 'nvim-linux64'; 93 | default: 94 | break; 95 | } 96 | } 97 | switch (arch) { 98 | case 'arm64': 99 | return 'nvim-linux-arm64'; 100 | case 'x86_64': 101 | return 'nvim-linux-x86_64'; 102 | default: 103 | throw Error(`Unsupported CPU architecture for Neovim ${version} on ${os}: ${arch}`); // Should be unreachable 104 | } 105 | } 106 | case 'windows': { 107 | // Until v0.6.1 release, 'Neovim' was the asset directory name on Windows. However it was changed to 'nvim-win64' 108 | // from v0.7.0. (#20) 109 | const v = parseVersion(version); 110 | if (v !== null && v.minor < 7) { 111 | return 'Neovim'; 112 | } 113 | return 'nvim-win64'; 114 | } 115 | } 116 | } 117 | 118 | async function unarchiveAsset(asset: string, dirName: string): Promise { 119 | const dir = path.dirname(asset); 120 | const dest = path.join(dir, dirName); 121 | if (asset.endsWith('.tar.gz')) { 122 | await exec('tar', ['xzf', asset], { cwd: dir }); 123 | return dest; 124 | } 125 | if (asset.endsWith('.zip')) { 126 | await unzip(asset, dir); 127 | return dest; 128 | } 129 | throw new Error(`FATAL: Don't know how to unarchive ${asset} to ${dest}`); 130 | } 131 | 132 | // version = 'stable' or 'nightly' or version string 133 | export async function downloadNeovim(version: string, os: Os, arch: Arch): Promise { 134 | const file = assetFileName(version, os, arch); 135 | const destDir = path.join(homedir(), `nvim-${version}`); 136 | const url = `https://github.com/neovim/neovim/releases/download/${version}/${file}`; 137 | console.log(`Downloading Neovim ${version} on ${os} from ${url} to ${destDir}`); 138 | 139 | const dlDir = await makeTmpdir(); 140 | const asset = path.join(dlDir, file); 141 | 142 | try { 143 | core.debug(`Downloading asset ${asset}`); 144 | const response = await fetch(url); 145 | if (!response.ok) { 146 | throw new Error(`Downloading asset failed: ${response.statusText}`); 147 | } 148 | const buffer = await response.arrayBuffer(); 149 | await fs.writeFile(asset, Buffer.from(buffer), { encoding: null }); 150 | core.debug(`Downloaded asset ${asset}`); 151 | 152 | const unarchived = await unarchiveAsset(asset, assetDirName(version, os, arch)); 153 | core.debug(`Unarchived asset ${unarchived}`); 154 | 155 | await io.mv(unarchived, destDir); 156 | core.debug(`Installed Neovim ${version} on ${os} to ${destDir}`); 157 | 158 | return { 159 | executable: exeName(os), 160 | binDir: path.join(destDir, 'bin'), 161 | }; 162 | } catch (e) { 163 | const err = ensureError(e); 164 | core.debug(err.stack ?? err.message); 165 | let msg = `Could not download Neovim release from ${url}: ${err.message}. Please visit https://github.com/neovim/neovim/releases/tag/${version} to check the asset for ${os} was really uploaded`; 166 | if (version === 'nightly') { 167 | msg += ". Note that some assets are sometimes missing on nightly build due to Neovim's CI failure"; 168 | } 169 | throw new Error(msg); 170 | } 171 | } 172 | 173 | async function fetchLatestVersion(token: string): Promise { 174 | const octokit = github.getOctokit(token); 175 | const { data } = await octokit.rest.repos.listReleases({ owner: 'neovim', repo: 'neovim' }); 176 | const re = /^v\d+\.\d+\.\d+$/; 177 | for (const release of data) { 178 | const tagName = release.tag_name; 179 | if (re.test(tagName)) { 180 | core.debug(`Detected the latest stable version '${tagName}'`); 181 | return tagName; 182 | } 183 | } 184 | core.debug(`No stable version was found in releases: ${JSON.stringify(data, null, 2)}`); 185 | throw new Error(`No stable version was found in ${data.length} releases`); 186 | } 187 | 188 | // Download stable asset from 'stable' release. When the asset is not found, get the latest version 189 | // using GitHub API and retry downloading an asset with the version as fallback (#5). 190 | export async function downloadStableNeovim(os: Os, arch: Arch, token: string | null = null): Promise { 191 | try { 192 | return await downloadNeovim('stable', os, arch); // `await` is necessary to catch excetipn 193 | } catch (e) { 194 | const err = ensureError(e); 195 | if (err.message.includes('Downloading asset failed:') && token !== null) { 196 | core.warning( 197 | `Could not download stable asset. Detecting the latest stable release from GitHub API as fallback: ${err.message}`, 198 | ); 199 | const ver = await fetchLatestVersion(token); 200 | core.warning(`Fallback to install asset from '${ver}' release`); 201 | return downloadNeovim(ver, os, arch); 202 | } 203 | throw err; 204 | } 205 | } 206 | 207 | // Build nightly Neovim from sources as fallback of downloading nightly assets from the nightly release page of 208 | // neovim/neovim repository (#18). 209 | // https://github.com/neovim/neovim/wiki/Building-Neovim 210 | export async function buildNightlyNeovim(os: Os): Promise { 211 | core.debug(`Installing Neovim by building from source on ${os}`); 212 | 213 | switch (os) { 214 | case 'linux': 215 | core.debug('Installing build dependencies via apt'); 216 | await exec('sudo', [ 217 | 'apt-get', 218 | 'install', 219 | '-y', 220 | '--no-install-recommends', 221 | 'ninja-build', 222 | 'gettext', 223 | 'libtool', 224 | 'libtool-bin', 225 | 'autoconf', 226 | 'automake', 227 | 'cmake', 228 | 'g++', 229 | 'pkg-config', 230 | 'unzip', 231 | 'curl', 232 | ]); 233 | break; 234 | case 'macos': 235 | core.debug('Installing build dependencies via Homebrew'); 236 | await exec('brew', [ 237 | 'install', 238 | 'ninja', 239 | 'libtool', 240 | 'automake', 241 | 'cmake', 242 | 'pkg-config', 243 | 'gettext', 244 | 'curl', 245 | '--quiet', 246 | ]); 247 | break; 248 | default: 249 | throw new Error(`Building Neovim from source is not supported for ${os} platform`); 250 | } 251 | 252 | // Add -nightly suffix since building stable Neovim from source may be supported in the future 253 | const installDir = path.join(homedir(), 'nvim-nightly'); 254 | core.debug(`Building and installing Neovim to ${installDir}`); 255 | const dir = path.join(await makeTmpdir(), 'build-nightly-neovim'); 256 | 257 | await exec('git', ['clone', '--depth=1', 'https://github.com/neovim/neovim.git', dir]); 258 | 259 | const opts = { cwd: dir }; 260 | const makeArgs = ['-j', `CMAKE_EXTRA_FLAGS=-DCMAKE_INSTALL_PREFIX=${installDir}`, 'CMAKE_BUILD_TYPE=RelWithDebug']; 261 | await exec('make', makeArgs, opts); 262 | core.debug(`Built Neovim in ${opts.cwd}. Installing it via 'make install'`); 263 | await exec('make', ['install'], opts); 264 | core.debug(`Installed Neovim to ${installDir}`); 265 | 266 | return { 267 | executable: exeName(os), 268 | binDir: path.join(installDir, 'bin'), 269 | }; 270 | } 271 | -------------------------------------------------------------------------------- /src/shell.ts: -------------------------------------------------------------------------------- 1 | import { type Buffer } from 'node:buffer'; 2 | import process from 'node:process'; 3 | import { exec as origExec } from '@actions/exec'; 4 | 5 | export type Env = Record; 6 | 7 | interface Options { 8 | readonly cwd?: string; 9 | readonly env?: Env; 10 | } 11 | 12 | // Avoid leaking $INPUT_* variables to subprocess 13 | // ref: https://github.com/actions/toolkit/issues/309 14 | function getEnv(base?: Env): Env { 15 | const ret: Env = base ?? {}; 16 | for (const key of Object.keys(process.env)) { 17 | if (!key.startsWith('INPUT_')) { 18 | const v = process.env[key]; 19 | if (v !== undefined) { 20 | ret[key] = v; 21 | } 22 | } 23 | } 24 | return ret; 25 | } 26 | 27 | export async function exec(cmd: string, args: string[], opts?: Options): Promise { 28 | const res = { 29 | stdout: '', 30 | stderr: '', 31 | }; 32 | 33 | const execOpts = { 34 | cwd: opts?.cwd, 35 | env: getEnv(opts?.env), 36 | listeners: { 37 | stdout(data: Buffer): void { 38 | res.stdout += data.toString(); 39 | }, 40 | stderr(data: Buffer): void { 41 | res.stderr += data.toString(); 42 | }, 43 | }, 44 | ignoreReturnCode: true, // Check exit status by myself for better error message 45 | }; 46 | 47 | const code = await origExec(cmd, args, execOpts); 48 | 49 | if (code === 0) { 50 | return res.stdout; 51 | } else { 52 | const stderr = res.stderr.replace(/\r?\n/g, ' '); 53 | throw new Error(`Command '${cmd} ${args.join(' ')}' exited non-zero status ${code}: ${stderr}`); 54 | } 55 | } 56 | 57 | const IS_DEBUG = !!process.env['RUNNER_DEBUG']; 58 | 59 | export async function unzip(file: string, cwd: string): Promise { 60 | // Suppress large output on unarchiving assets when RUNNER_DEBUG is not set (#25) 61 | const args = IS_DEBUG ? [file] : ['-q', file]; 62 | await exec('unzip', args, { cwd }); 63 | } 64 | -------------------------------------------------------------------------------- /src/system.ts: -------------------------------------------------------------------------------- 1 | import { tmpdir } from 'node:os'; 2 | import process from 'node:process'; 3 | import * as core from '@actions/core'; 4 | import { mkdirP } from '@actions/io'; 5 | 6 | export type Os = 'macos' | 'linux' | 'windows'; 7 | export type Arch = 'arm64' | 'x86_64' | 'arm32'; 8 | 9 | export async function makeTmpdir(): Promise { 10 | const dir = tmpdir(); 11 | await mkdirP(dir); 12 | core.debug(`Created temporary directory ${dir}`); 13 | return dir; 14 | } 15 | 16 | export function getOs(): Os { 17 | switch (process.platform) { 18 | case 'darwin': 19 | return 'macos'; 20 | case 'linux': 21 | return 'linux'; 22 | case 'win32': 23 | return 'windows'; 24 | default: 25 | throw new Error(`Platform '${process.platform}' is not supported`); 26 | } 27 | } 28 | 29 | export function getArch(): Arch { 30 | switch (process.arch) { 31 | case 'arm': 32 | return 'arm32'; 33 | case 'arm64': 34 | return 'arm64'; 35 | case 'x64': 36 | return 'x86_64'; 37 | default: 38 | throw new Error(`CPU arch '${process.arch}' is not supported`); 39 | } 40 | } 41 | 42 | export function ensureError(err: unknown): Error { 43 | if (err instanceof Error) { 44 | return err; 45 | } 46 | // eslint-disable-next-line @typescript-eslint/restrict-template-expressions 47 | return new Error(`Unknown fatal error: ${err}`); 48 | } 49 | -------------------------------------------------------------------------------- /src/validate.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs, constants as fsconsts } from 'node:fs'; 2 | import { join } from 'node:path'; 3 | import * as core from '@actions/core'; 4 | import type { Installed } from './install.js'; 5 | import { exec } from './shell.js'; 6 | import { ensureError } from './system.js'; 7 | 8 | export async function validateInstallation(installed: Installed): Promise { 9 | try { 10 | const s = await fs.stat(installed.binDir); 11 | if (!s.isDirectory()) { 12 | throw new Error(`Validation failed! '${installed.binDir}' is not a directory for executable`); 13 | } 14 | } catch (e) { 15 | const err = ensureError(e); 16 | throw new Error(`Validation failed! Could not stat installed directory '${installed.binDir}': ${err.message}`); 17 | } 18 | core.debug(`Installed directory '${installed.binDir}' was validated`); 19 | 20 | const fullPath = join(installed.binDir, installed.executable); 21 | 22 | try { 23 | await fs.access(fullPath, fsconsts.X_OK); 24 | } catch (e) { 25 | const err = ensureError(e); 26 | throw new Error(`Validation failed! Could not access the installed executable '${fullPath}': ${err.message}`); 27 | } 28 | 29 | try { 30 | const ver = await exec(fullPath, ['--version']); 31 | console.log(`Installed version:\n${ver}`); 32 | } catch (e) { 33 | const err = ensureError(e); 34 | throw new Error(`Validation failed! Could not get version from executable '${fullPath}': ${err.message}`); 35 | } 36 | core.debug(`Installed executable '${fullPath}' was validated`); 37 | } 38 | -------------------------------------------------------------------------------- /src/vim.ts: -------------------------------------------------------------------------------- 1 | import { homedir } from 'node:os'; 2 | import * as path from 'node:path'; 3 | import { promises as fs } from 'node:fs'; 4 | import { strict as assert } from 'node:assert'; 5 | import { Buffer } from 'node:buffer'; 6 | import process from 'node:process'; 7 | import fetch from 'node-fetch'; 8 | import * as core from '@actions/core'; 9 | import * as io from '@actions/io'; 10 | import { split as shlexSplit } from 'shlex'; 11 | import { exec, unzip, Env } from './shell.js'; 12 | import { makeTmpdir, type Os, ensureError } from './system.js'; 13 | import type { Installed, ExeName } from './install.js'; 14 | 15 | function exeName(os: Os): ExeName { 16 | return os === 'windows' ? 'vim.exe' : 'vim'; 17 | } 18 | 19 | export function versionIsOlderThan(version: string, vmajor: number, vminor: number, vpatch: number): boolean { 20 | // Note: Patch version may not exist on v7 or earlier 21 | const majorStr = version.match(/^v(\d+)\./)?.[1]; 22 | if (!majorStr) { 23 | return false; // Invalid case. Should be unreachable 24 | } 25 | const major = parseInt(majorStr, 10); 26 | 27 | if (major !== vmajor) { 28 | return major < vmajor; 29 | } 30 | 31 | const m = version.match(/\.(\d+)\.(\d{4})$/); // Extract minor and patch versions 32 | if (!m) { 33 | return false; // Invalid case. Should be unreachable 34 | } 35 | 36 | const minor = parseInt(m[1], 10); 37 | if (minor !== vminor) { 38 | return minor < vminor; 39 | } 40 | 41 | const patch = parseInt(m[2], 10); 42 | return patch < vpatch; 43 | } 44 | 45 | async function getXcode11DevDir(): Promise { 46 | // Xcode10~12 are available at this point: 47 | // https://github.com/actions/virtual-environments/blob/main/images/macos/macos-10.15-Readme.md#xcode 48 | // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing 49 | const dir = process.env['XCODE_11_DEVELOPER_DIR'] || '/Applications/Xcode_11.7.app/Contents/Developer'; 50 | try { 51 | await fs.access(dir); 52 | return dir; 53 | } catch (/* eslint-disable-line @typescript-eslint/no-unused-vars */ e) { 54 | return null; 55 | } 56 | } 57 | 58 | // Only available on macOS or Linux. Passing null to `version` means install HEAD 59 | export async function buildVim(version: string, os: Os, configureArgs: string | null): Promise { 60 | assert.notEqual(version, 'stable'); 61 | const installDir = path.join(homedir(), `vim-${version}`); 62 | core.debug(`Building and installing Vim to ${installDir} (version=${version ?? 'HEAD'})`); 63 | const dir = path.join(await makeTmpdir(), 'vim'); 64 | 65 | { 66 | const args = ['clone', '--depth=1', '--single-branch']; 67 | if (version === 'nightly') { 68 | args.push('--no-tags'); 69 | } else { 70 | args.push('--branch', version); 71 | } 72 | args.push('https://github.com/vim/vim', dir); 73 | 74 | await exec('git', args); 75 | } 76 | 77 | const env: Env = {}; 78 | if (os === 'macos' && versionIsOlderThan(version, 8, 2, 1119)) { 79 | const dir = await getXcode11DevDir(); 80 | if (dir !== null) { 81 | // Vim before v8.2.1119 cannot be built with Xcode 12 or later. It requires Xcode 11. 82 | // ref: https://github.com/vim/vim/commit/5289783e0b07cfc3f92ee933261ca4c4acdca007 83 | // By setting $DEVELOPER_DIR environment variable, Xcode11 is used to build Vim. 84 | // ref: https://www.jessesquires.com/blog/2020/01/06/selecting-an-xcode-version-on-github-ci/ 85 | // Note that xcode-select command is not available since it changes Xcode version in system global. 86 | env['DEVELOPER_DIR'] = dir; 87 | core.debug(`Building Vim older than 8.2.1119 on macOS with Xcode11 at ${dir} instead of the latest Xcode`); 88 | } else { 89 | core.warning( 90 | `Building Vim older than 8.2.1119 on macOS needs Xcode11 but proper Xcode is not found at ${dir}. Using the latest Xcode as fallback. If you're using macos-latest or macos-12 runner and see some build error, try macos-11 runner`, 91 | ); 92 | } 93 | } 94 | 95 | const opts = { cwd: dir, env }; 96 | { 97 | const args = [`--prefix=${installDir}`]; 98 | if (configureArgs === null) { 99 | args.push('--with-features=huge', '--enable-fail-if-missing'); 100 | } else { 101 | args.push(...shlexSplit(configureArgs)); 102 | } 103 | try { 104 | await exec('./configure', args, opts); 105 | } catch (err) { 106 | if (os === 'macos' && versionIsOlderThan(version, 8, 2, 5135)) { 107 | core.warning( 108 | 'This version of Vim has a bug where ./configure cannot find a terminal library correctly. See the following issue for more details: https://github.com/rhysd/action-setup-vim/issues/38', 109 | ); 110 | } 111 | throw err; 112 | } 113 | } 114 | await exec('make', ['-j'], opts); 115 | await exec('make', ['install'], opts); 116 | core.debug(`Built and installed Vim to ${installDir} (version=${version})`); 117 | 118 | return { 119 | executable: exeName(os), 120 | binDir: path.join(installDir, 'bin'), 121 | }; 122 | } 123 | 124 | async function getVimRootDirAt(dir: string): Promise { 125 | // Search root Vim directory such as 'vim82' in unarchived directory 126 | const entries = await fs.readdir(dir); 127 | const re = /^vim\d+$/; 128 | for (const entry of entries) { 129 | if (!re.test(entry)) { 130 | continue; 131 | } 132 | const p = path.join(dir, entry); 133 | const s = await fs.stat(p); 134 | if (!s.isDirectory()) { 135 | continue; 136 | } 137 | return p; 138 | } 139 | throw new Error( 140 | `Vim directory such as 'vim82' was not found in ${JSON.stringify(entries)} in unarchived directory '${dir}'`, 141 | ); 142 | } 143 | 144 | export async function detectLatestWindowsReleaseTag(): Promise { 145 | const url = 'https://github.com/vim/vim-win32-installer/releases/latest'; 146 | try { 147 | const res = await fetch(url, { 148 | method: 'HEAD', 149 | redirect: 'manual', 150 | }); 151 | 152 | if (res.status !== 302) { 153 | throw new Error(`Expected status 302 (Redirect) but got ${res.status} (${res.statusText})`); 154 | } 155 | 156 | const location = res.headers.get('location'); 157 | if (!location) { 158 | throw new Error(`'Location' header is not included in a response: ${JSON.stringify(res.headers.raw())}`); 159 | } 160 | 161 | const m = location.match(/\/releases\/tag\/(.+)$/); 162 | if (m === null) { 163 | throw new Error(`Unexpected redirect to ${location}. Redirected URL is not for release`); 164 | } 165 | 166 | core.debug(`Latest Vim release tag ${m[1]} was extracted from redirect`); 167 | return m[1]; 168 | } catch (e) { 169 | const err = ensureError(e); 170 | core.debug(err.stack ?? err.message); 171 | throw new Error(`${err.message}: Could not get latest release tag from ${url}`); 172 | } 173 | } 174 | 175 | async function installVimAssetOnWindows(file: string, url: string, dirSuffix: string): Promise { 176 | const tmpdir = await makeTmpdir(); 177 | const dlDir = path.join(tmpdir, 'vim-installer'); 178 | await io.mkdirP(dlDir); 179 | const assetFile = path.join(dlDir, file); 180 | 181 | try { 182 | core.debug(`Downloading asset at ${url} to ${dlDir}`); 183 | const response = await fetch(url); 184 | if (!response.ok) { 185 | throw new Error(`Downloading asset failed: ${response.statusText}`); 186 | } 187 | const buffer = await response.buffer(); 188 | await fs.writeFile(assetFile, Buffer.from(buffer), { encoding: null }); 189 | core.debug(`Downloaded installer from ${url} to ${assetFile}`); 190 | 191 | await unzip(assetFile, dlDir); 192 | } catch (e) { 193 | const err = ensureError(e); 194 | core.debug(err.stack ?? err.message); 195 | throw new Error(`Could not download and unarchive asset ${url} at ${dlDir}: ${err.message}`); 196 | } 197 | 198 | const unzippedDir = path.join(dlDir, 'vim'); // Unarchived to 'vim' directory 199 | const vimDir = await getVimRootDirAt(unzippedDir); 200 | core.debug(`Unzipped installer from ${url} and found Vim directory ${vimDir}`); 201 | 202 | const destDir = path.join(homedir(), `vim-${dirSuffix}`); 203 | await io.mv(vimDir, destDir); 204 | core.debug(`Vim was installed to ${destDir}`); 205 | 206 | return destDir; 207 | } 208 | 209 | export async function installVimOnWindows(tag: string, version: string): Promise { 210 | const ver = tag.slice(1); // Strip 'v' prefix 211 | // e.g. https://github.com/vim/vim-win32-installer/releases/download/v8.2.0158/gvim_8.2.0158_x64.zip 212 | const url = `https://github.com/vim/vim-win32-installer/releases/download/${tag}/gvim_${ver}_x64.zip`; 213 | const file = `gvim_${ver}_x64.zip`; 214 | const destDir = await installVimAssetOnWindows(file, url, version); 215 | const executable = exeName('windows'); 216 | 217 | // From v9.1.0631, vim.exe and gvim.exe share the same core, so OLE is enabled even in vim.exe. 218 | // This command registers the vim64.dll as a type library. Without the command, vim.exe will 219 | // ask the registration with GUI dialog and the process looks hanging. (#37) 220 | // 221 | // See: https://github.com/vim/vim/issues/15372 222 | if (version === 'stable' || version === 'nightly' || !versionIsOlderThan(version, 9, 1, 631)) { 223 | const bin = path.join(destDir, executable); 224 | await exec(bin, ['-silent', '-register']); 225 | core.debug('Registered vim.exe as a type library'); 226 | } 227 | 228 | return { executable, binDir: destDir }; 229 | } 230 | 231 | export async function installNightlyVimOnWindows(version: string): Promise { 232 | const latestTag = await detectLatestWindowsReleaseTag(); 233 | return installVimOnWindows(latestTag, version); 234 | } 235 | -------------------------------------------------------------------------------- /test/config.ts: -------------------------------------------------------------------------------- 1 | import { strict as A } from 'node:assert'; 2 | import process from 'node:process'; 3 | import { loadConfigFromInputs } from '../src/config.js'; 4 | 5 | function setInputs(inputs: Record): void { 6 | for (const key of Object.keys(inputs)) { 7 | const k = `INPUT_${key.toUpperCase().replace(' ', '_')}`; 8 | process.env[k] = inputs[key]; 9 | } 10 | } 11 | 12 | describe('loadConfigFromInputs()', function () { 13 | let savedEnv: Record; 14 | 15 | before(function () { 16 | savedEnv = { ...process.env }; 17 | }); 18 | 19 | afterEach(function () { 20 | process.env = { ...savedEnv }; 21 | }); 22 | 23 | it('returns default configurations with no input', function () { 24 | const c = loadConfigFromInputs(); 25 | A.equal(c.version, 'stable'); 26 | A.equal(c.neovim, false); 27 | A.equal(c.configureArgs, null); 28 | A.ok(['macos', 'linux', 'windows'].includes(c.os), c.os); 29 | A.ok(['arm64', 'x86_64'].includes(c.arch), c.arch); 30 | }); 31 | 32 | it('returns validated configurations with user inputs', function () { 33 | setInputs({ 34 | version: 'nightly', 35 | neovim: 'true', 36 | 'configure-args': '--with-features=huge --disable-nls', 37 | }); 38 | const c = loadConfigFromInputs(); 39 | A.equal(c.version, 'nightly'); 40 | A.equal(c.neovim, true); 41 | A.equal(c.configureArgs, '--with-features=huge --disable-nls'); 42 | A.ok(['macos', 'linux', 'windows'].includes(c.os), c.os); 43 | A.ok(['arm64', 'x86_64'].includes(c.arch), c.arch); 44 | }); 45 | 46 | for (const version of ['STABLE', 'Nightly']) { 47 | it(`sets '${version}' for ${version.toLowerCase()}`, function () { 48 | setInputs({ version }); 49 | const c = loadConfigFromInputs(); 50 | A.equal(c.version, version.toLowerCase()); 51 | }); 52 | } 53 | 54 | for (const b of ['TRUE', 'False']) { 55 | it(`sets '${b}' for boolean value ${b.toLowerCase()}`, function () { 56 | setInputs({ neovim: b }); 57 | const c = loadConfigFromInputs(); 58 | const expected = b.toLowerCase() === 'true'; 59 | A.equal(c.neovim, expected); 60 | }); 61 | } 62 | 63 | const specificVersions: Array<{ 64 | neovim: boolean; 65 | version: string; 66 | }> = [ 67 | { 68 | neovim: false, 69 | version: 'v8.1.1111', 70 | }, 71 | { 72 | neovim: false, 73 | version: 'v8.2.0001', 74 | }, 75 | { 76 | neovim: false, 77 | version: 'v10.10.0001', 78 | }, 79 | { 80 | neovim: false, 81 | version: 'v7.4.100', 82 | }, 83 | { 84 | neovim: false, 85 | version: 'v7.4', 86 | }, 87 | { 88 | neovim: true, 89 | version: 'v0.4.3', 90 | }, 91 | { 92 | neovim: true, 93 | version: 'v1.0.0', 94 | }, 95 | { 96 | neovim: true, 97 | version: 'v10.10.10', 98 | }, 99 | ]; 100 | 101 | for (const t of specificVersions) { 102 | const editor = t.neovim ? 'Neovim' : 'Vim'; 103 | 104 | it(`verifies correct ${editor} version ${t.version}`, function () { 105 | setInputs({ 106 | version: t.version, 107 | neovim: t.neovim.toString(), 108 | }); 109 | const c = loadConfigFromInputs(); 110 | A.equal(c.version, t.version); 111 | A.equal(c.neovim, t.neovim); 112 | }); 113 | } 114 | 115 | const errorCases: Array<{ 116 | what: string; 117 | inputs: Record; 118 | expected: RegExp; 119 | }> = [ 120 | { 121 | what: 'wrong neovim input', 122 | inputs: { 123 | neovim: 'latest', 124 | }, 125 | expected: /'neovim' input only accepts boolean values 'true' or 'false' but got 'latest'/, 126 | }, 127 | { 128 | what: 'vim version with wrong number of digits in patch version', 129 | inputs: { 130 | version: 'v8.2.100', 131 | }, 132 | expected: /'version' input 'v8\.2\.100' is not a format of Git tags in vim\/vim repository/, 133 | }, 134 | { 135 | what: 'vim version without prefix "v"', 136 | inputs: { 137 | version: '8.2.0100', 138 | }, 139 | expected: /'version' input '8\.2\.0100' is not a format of Git tags in vim\/vim repository/, 140 | }, 141 | { 142 | what: 'vim version with patch version', 143 | inputs: { 144 | version: 'v8.2', 145 | }, 146 | expected: /'version' input 'v8\.2' is not a format of Git tags in vim\/vim repository/, 147 | }, 148 | { 149 | what: 'vim version with only major version', 150 | inputs: { 151 | version: 'v8', 152 | }, 153 | expected: /'version' input 'v8' is not a format of Git tags in vim\/vim repository/, 154 | }, 155 | { 156 | what: 'vim version with wrong tag name', 157 | inputs: { 158 | version: 'latest', 159 | }, 160 | expected: /'version' input 'latest' is not a format of Git tags in vim\/vim repository/, 161 | }, 162 | { 163 | what: 'neovim version without prefix "v"', 164 | inputs: { 165 | neovim: 'true', 166 | version: '0.4.3', 167 | }, 168 | expected: /'version' input '0\.4\.3' is not a format of Git tags in neovim\/neovim repository/, 169 | }, 170 | { 171 | what: 'neovim version without patch version', 172 | inputs: { 173 | neovim: 'true', 174 | version: 'v0.4', 175 | }, 176 | expected: /'version' input 'v0\.4' is not a format of Git tags in neovim\/neovim repository/, 177 | }, 178 | { 179 | what: 'neovim version with only major version', 180 | inputs: { 181 | neovim: 'true', 182 | version: 'v1', 183 | }, 184 | expected: /'version' input 'v1' is not a format of Git tags in neovim\/neovim repository/, 185 | }, 186 | { 187 | what: 'neovim version with wrong tag name', 188 | inputs: { 189 | neovim: 'true', 190 | version: 'latest', 191 | }, 192 | expected: /'version' input 'latest' is not a format of Git tags in neovim\/neovim repository/, 193 | }, 194 | ]; 195 | 196 | for (const t of errorCases) { 197 | it(`causes an error on ${t.what}`, function () { 198 | setInputs(t.inputs); 199 | A.throws(loadConfigFromInputs, t.expected); 200 | }); 201 | } 202 | }); 203 | -------------------------------------------------------------------------------- /test/helper.ts: -------------------------------------------------------------------------------- 1 | import { Response } from 'node-fetch'; 2 | import esmock from 'esmock'; 3 | 4 | function mockedFetch(url: string): Promise { 5 | const notFound = { status: 404, statusText: 'Not found for dummy' }; 6 | return Promise.resolve(new Response(`dummy response for ${url}`, notFound)); 7 | } 8 | 9 | export function importFetchMocked(path: string): Promise { 10 | return esmock(path, {}, { 'node-fetch': { default: mockedFetch } }); 11 | } 12 | 13 | // Arguments of exec(): cmd: string, args: string[], options?: Options 14 | export type ExecArgs = [string, string[], { env: Record } | undefined]; 15 | export class ExecStub { 16 | called: ExecArgs[] = []; 17 | 18 | onCalled(args: ExecArgs): void { 19 | this.called.push(args); 20 | } 21 | 22 | reset(): void { 23 | this.called = []; 24 | } 25 | 26 | mockedExec(...args: ExecArgs): Promise { 27 | this.onCalled(args); 28 | return Promise.resolve(''); 29 | } 30 | 31 | importWithMock(path: string): Promise { 32 | const exec = this.mockedExec.bind(this); 33 | return esmock(path, {}, { '../src/shell.js': { exec } }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/install_linux.ts: -------------------------------------------------------------------------------- 1 | import { strict as A } from 'node:assert'; 2 | import { type install } from '../src/install_linux.js'; 3 | import { type Config } from '../src/config.js'; 4 | import { ExecStub } from './helper.js'; 5 | 6 | describe('Installation on Linux', function () { 7 | const stub = new ExecStub(); 8 | let installMocked: typeof install; 9 | 10 | before(async function () { 11 | const { install } = await stub.importWithMock('../src/install_linux.js'); 12 | installMocked = install; 13 | }); 14 | 15 | afterEach(function () { 16 | stub.reset(); 17 | }); 18 | 19 | it('installs stable Vim by apt-get', async function () { 20 | const config: Config = { 21 | version: 'stable', 22 | neovim: false, 23 | os: 'linux', 24 | arch: 'x86_64', 25 | configureArgs: null, 26 | token: null, 27 | }; 28 | 29 | const installed = await installMocked(config); 30 | A.equal(installed.executable, 'vim'); 31 | A.equal(installed.binDir, '/usr/bin'); 32 | 33 | A.deepEqual(stub.called[0], ['sudo', ['apt-get', 'update', '-y', '-q']]); 34 | A.deepEqual(stub.called[1], [ 35 | 'sudo', 36 | ['apt-get', 'install', '-y', '--no-install-recommends', '-q', 'vim-gtk3'], 37 | ]); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/install_macos.ts: -------------------------------------------------------------------------------- 1 | import { strict as A } from 'node:assert'; 2 | import { type install } from '../src/install_macos.js'; 3 | import { type Config } from '../src/config.js'; 4 | import { ExecStub } from './helper.js'; 5 | 6 | describe('Installation on macOS', function () { 7 | const stub = new ExecStub(); 8 | let installMocked: typeof install; 9 | 10 | before(async function () { 11 | const { install } = await stub.importWithMock('../src/install_macos.js'); 12 | installMocked = install; 13 | }); 14 | 15 | afterEach(function () { 16 | stub.reset(); 17 | }); 18 | 19 | it('installs stable Neovim from Homebrew', async function () { 20 | const config: Config = { 21 | version: 'stable', 22 | neovim: true, 23 | os: 'macos', 24 | arch: 'arm64', 25 | configureArgs: null, 26 | token: null, 27 | }; 28 | 29 | const installed = await installMocked(config); 30 | A.equal(installed.executable, 'nvim'); 31 | A.equal(installed.binDir, '/opt/homebrew/bin'); 32 | 33 | A.deepEqual(stub.called[0], ['brew', ['update', '--quiet']]); 34 | A.deepEqual(stub.called[1], ['brew', ['install', 'neovim', '--quiet']]); 35 | }); 36 | 37 | it('installs stable Vim from Homebrew', async function () { 38 | const config: Config = { 39 | version: 'stable', 40 | neovim: false, 41 | os: 'macos', 42 | arch: 'arm64', 43 | configureArgs: null, 44 | token: null, 45 | }; 46 | 47 | const installed = await installMocked(config); 48 | A.equal(installed.executable, 'vim'); 49 | A.equal(installed.binDir, '/opt/homebrew/bin'); 50 | 51 | A.deepEqual(stub.called[0], ['brew', ['update', '--quiet']]); 52 | A.deepEqual(stub.called[1], ['brew', ['install', 'macvim', '--quiet']]); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/neovim.ts: -------------------------------------------------------------------------------- 1 | import { strict as A } from 'node:assert'; 2 | import * as path from 'node:path'; 3 | import process from 'node:process'; 4 | import { 5 | downloadNeovim, 6 | type downloadStableNeovim, 7 | type buildNightlyNeovim, 8 | assetDirName, 9 | assetFileName, 10 | } from '../src/neovim.js'; 11 | import { importFetchMocked, ExecStub } from './helper.js'; 12 | 13 | describe('Neovim installation', function () { 14 | describe('downloadNeovim()', function () { 15 | it('throws an error when release asset not found', async function () { 16 | await A.rejects(() => downloadNeovim('v0.4.999', 'linux', 'x86_64'), /Downloading asset failed/); 17 | }); 18 | 19 | context('with mocking fetch()', function () { 20 | let downloadNeovimMocked: typeof downloadNeovim; 21 | let downloadStableNeovimMocked: typeof downloadStableNeovim; 22 | 23 | before(async function () { 24 | const { downloadNeovim, downloadStableNeovim } = await importFetchMocked('../src/neovim.js'); 25 | downloadNeovimMocked = downloadNeovim; 26 | downloadStableNeovimMocked = downloadStableNeovim; 27 | }); 28 | 29 | it('throws an error when receiving unsuccessful response', async function () { 30 | try { 31 | const ret = await downloadNeovimMocked('nightly', 'linux', 'x86_64'); 32 | A.ok(false, `Exception was not thrown: ${JSON.stringify(ret)}`); 33 | } catch (err) { 34 | const msg = (err as Error).message; 35 | A.ok(msg.includes('Could not download Neovim release from'), msg); 36 | A.ok(msg.includes('check the asset for linux was really uploaded'), msg); 37 | // Special message only for nightly build 38 | A.ok(msg.includes('Note that some assets are sometimes missing on nightly build'), msg); 39 | } 40 | }); 41 | 42 | it('fallbacks to the latest version detected from GitHub API', async function () { 43 | const token = process.env['GITHUB_TOKEN'] ?? null; 44 | if (token === null) { 45 | this.skip(); // GitHub API token is necessary 46 | } 47 | try { 48 | const ret = await downloadStableNeovimMocked('linux', 'x86_64', token); 49 | A.ok(false, `Exception was not thrown: ${JSON.stringify(ret)}`); 50 | } catch (e) { 51 | const err = e as Error; 52 | // Matches to version tag like '/v0.4.4/' as part of download URL in error message 53 | // Note: assert.match is not available in Node v12 54 | A.ok(/\/v\d+\.\d+\.\d+\//.test(err.message), err.message); 55 | } 56 | }); 57 | }); 58 | }); 59 | 60 | describe('buildNightlyNeovim()', function () { 61 | const stub = new ExecStub(); 62 | let buildNightlyNeovimMocked: typeof buildNightlyNeovim; 63 | 64 | before(async function () { 65 | const { buildNightlyNeovim } = await stub.importWithMock('../src/neovim.js'); 66 | buildNightlyNeovimMocked = buildNightlyNeovim; 67 | }); 68 | 69 | afterEach(function () { 70 | stub.reset(); 71 | }); 72 | 73 | it('builds nightly Neovim on Linux', async function () { 74 | const installed = await buildNightlyNeovimMocked('linux'); 75 | A.equal(installed.executable, 'nvim'); 76 | A.ok(installed.binDir.endsWith(path.join('nvim-nightly', 'bin')), installed.binDir); 77 | const installDir = path.dirname(installed.binDir); 78 | 79 | // apt-get -> git -> make 80 | const apt = stub.called[0]; 81 | A.ok(apt[0] === 'sudo' && apt[1][0] === 'apt-get', JSON.stringify(apt)); 82 | const make = stub.called[2]; 83 | A.equal(make[0], 'make'); 84 | const makeArgs = make[1]; 85 | A.ok(makeArgs[1].endsWith(installDir), `${makeArgs}`); 86 | }); 87 | 88 | it('builds nightly Neovim on macOS', async function () { 89 | const installed = await buildNightlyNeovimMocked('macos'); 90 | A.equal(installed.executable, 'nvim'); 91 | A.ok(installed.binDir.endsWith(path.join('nvim-nightly', 'bin')), installed.binDir); 92 | const installDir = path.dirname(installed.binDir); 93 | 94 | // brew -> git -> make 95 | const brew = stub.called[0]; 96 | A.ok(brew[0] === 'brew', JSON.stringify(brew)); 97 | const make = stub.called[2]; 98 | A.equal(make[0], 'make'); 99 | const makeArgs = make[1]; 100 | A.ok(makeArgs[1].endsWith(installDir), `${makeArgs}`); 101 | }); 102 | 103 | it('throws an error on Windows', async function () { 104 | await A.rejects( 105 | () => buildNightlyNeovimMocked('windows'), 106 | /Building Neovim from source is not supported for windows/, 107 | ); 108 | }); 109 | }); 110 | 111 | describe('assetDirName', function () { 112 | it('returns "Neovim" when Neovim version is earlier than 0.7 on Windows', function () { 113 | A.equal(assetDirName('v0.6.1', 'windows', 'x86_64'), 'Neovim'); 114 | A.equal(assetDirName('v0.4.3', 'windows', 'x86_64'), 'Neovim'); 115 | }); 116 | 117 | it('returns "nvim-win64" when Neovim version is 0.7 or later on Windows', function () { 118 | A.equal(assetDirName('v0.7.0', 'windows', 'x86_64'), 'nvim-win64'); 119 | A.equal(assetDirName('v0.10.0', 'windows', 'x86_64'), 'nvim-win64'); 120 | A.equal(assetDirName('v1.0.0', 'windows', 'x86_64'), 'nvim-win64'); 121 | A.equal(assetDirName('nightly', 'windows', 'x86_64'), 'nvim-win64'); 122 | A.equal(assetDirName('stable', 'windows', 'x86_64'), 'nvim-win64'); 123 | }); 124 | 125 | it('returns "nvim-osx64" when Neovim version is earlier than 0.7.1 on macOS', function () { 126 | A.equal(assetDirName('v0.7.0', 'macos', 'x86_64'), 'nvim-osx64'); 127 | A.equal(assetDirName('v0.6.1', 'macos', 'x86_64'), 'nvim-osx64'); 128 | }); 129 | 130 | it('returns "nvim-macos" when Neovim version is 0.7.1 or later and 0.9.5 or earlier on macOS', function () { 131 | A.equal(assetDirName('v0.7.1', 'macos', 'x86_64'), 'nvim-macos'); 132 | A.equal(assetDirName('v0.8.0', 'macos', 'x86_64'), 'nvim-macos'); 133 | A.equal(assetDirName('v0.9.5', 'macos', 'x86_64'), 'nvim-macos'); 134 | }); 135 | 136 | it('returns "nvim-macos-arm64" or "nvim-macos-x86_64" based on the CPU arch when Neovim version is 0.10.0 later on macOS', function () { 137 | A.equal(assetDirName('v0.10.0', 'macos', 'x86_64'), 'nvim-macos-x86_64'); 138 | A.equal(assetDirName('v1.0.0', 'macos', 'x86_64'), 'nvim-macos-x86_64'); 139 | A.equal(assetDirName('stable', 'macos', 'x86_64'), 'nvim-macos-x86_64'); 140 | A.equal(assetDirName('nightly', 'macos', 'x86_64'), 'nvim-macos-x86_64'); 141 | A.equal(assetDirName('v0.10.0', 'macos', 'arm64'), 'nvim-macos-arm64'); 142 | A.equal(assetDirName('v1.0.0', 'macos', 'arm64'), 'nvim-macos-arm64'); 143 | A.equal(assetDirName('stable', 'macos', 'arm64'), 'nvim-macos-arm64'); 144 | A.equal(assetDirName('nightly', 'macos', 'arm64'), 'nvim-macos-arm64'); 145 | }); 146 | 147 | it('returns "nvim-linux64" when Neovim version is earlier than 0.10.4 on Linux', function () { 148 | A.equal(assetDirName('v0.10.3', 'linux', 'x86_64'), 'nvim-linux64'); 149 | A.equal(assetDirName('v0.9.5', 'linux', 'x86_64'), 'nvim-linux64'); 150 | A.throws( 151 | () => assetDirName('v0.10.3', 'linux', 'arm64'), 152 | /^Error: Linux arm64 has been only supported since Neovim v0\.10\.4/, 153 | ); 154 | A.throws( 155 | () => assetDirName('v0.9.5', 'linux', 'arm64'), 156 | /^Error: Linux arm64 has been only supported since Neovim v0\.10\.4/, 157 | ); 158 | }); 159 | 160 | it('returns "nvim-linux-x86_64" or "nvim-linux-arm64" when Neovim version is earlier than 0.10.4 on Linux', function () { 161 | A.equal(assetDirName('v0.10.4', 'linux', 'x86_64'), 'nvim-linux-x86_64'); 162 | A.equal(assetDirName('v0.11.0', 'linux', 'x86_64'), 'nvim-linux-x86_64'); 163 | A.equal(assetDirName('v0.10.4', 'linux', 'arm64'), 'nvim-linux-arm64'); 164 | A.equal(assetDirName('v0.11.0', 'linux', 'arm64'), 'nvim-linux-arm64'); 165 | A.equal(assetDirName('stable', 'linux', 'x86_64'), 'nvim-linux-x86_64'); 166 | A.equal(assetDirName('stable', 'linux', 'arm64'), 'nvim-linux-arm64'); 167 | }); 168 | 169 | it('throws an error on arm32 Linux', function () { 170 | A.throws(() => assetDirName('v0.10.3', 'linux', 'arm32'), /^Error: Unsupported CPU architecture/); 171 | A.throws(() => assetDirName('stable', 'linux', 'arm32'), /^Error: Unsupported CPU architecture/); 172 | }); 173 | }); 174 | 175 | describe('assetFileName', function () { 176 | it('returns asset file name following the Neovim version and CPU arch on Linux', function () { 177 | A.equal(assetFileName('v0.10.3', 'linux', 'x86_64'), 'nvim-linux64.tar.gz'); 178 | A.equal(assetFileName('v0.10.4', 'linux', 'x86_64'), 'nvim-linux-x86_64.tar.gz'); 179 | A.equal(assetFileName('v0.10.4', 'linux', 'arm64'), 'nvim-linux-arm64.tar.gz'); 180 | }); 181 | 182 | it('returns asset file name following the Neovim version and CPU arch on macOS', function () { 183 | A.equal(assetFileName('v0.7.0', 'macos', 'x86_64'), 'nvim-macos.tar.gz'); 184 | A.equal(assetFileName('v0.7.1', 'macos', 'x86_64'), 'nvim-macos.tar.gz'); 185 | A.equal(assetFileName('v0.10.4', 'macos', 'x86_64'), 'nvim-macos-x86_64.tar.gz'); 186 | A.equal(assetFileName('v0.10.4', 'macos', 'arm64'), 'nvim-macos-arm64.tar.gz'); 187 | }); 188 | }); 189 | }); 190 | -------------------------------------------------------------------------------- /test/shell.ts: -------------------------------------------------------------------------------- 1 | import { strict as A } from 'node:assert'; 2 | import { Buffer } from 'node:buffer'; 3 | import process from 'node:process'; 4 | import esmock from 'esmock'; 5 | import { type exec } from '../src/shell.js'; 6 | 7 | class ExecSpy { 8 | public called: any[] = []; 9 | public exitCode = 0; 10 | 11 | async mockedExec(cmd: string, args: string[], opts?: any): Promise { 12 | this.called = [cmd, args, opts]; 13 | opts.listeners.stdout(Buffer.from('this is stdout')); 14 | opts.listeners.stderr(Buffer.from('this is stderr')); 15 | return Promise.resolve(this.exitCode); 16 | } 17 | 18 | reset(): void { 19 | this.called = []; 20 | this.exitCode = 0; 21 | } 22 | 23 | mockedImport(): Promise { 24 | return esmock( 25 | '../src/shell.js', 26 | {}, 27 | { 28 | '@actions/exec': { 29 | exec: this.mockedExec.bind(this), 30 | }, 31 | }, 32 | ); 33 | } 34 | } 35 | 36 | describe('shell', function () { 37 | // let unzip: (file: string, cwd: string) => Promise; 38 | const spy = new ExecSpy(); 39 | const savedDebugEnv = process.env['RUNNER_DEBUG']; 40 | 41 | after(function () { 42 | process.env['RUNNER_DEBUG'] = savedDebugEnv; 43 | }); 44 | 45 | afterEach(function () { 46 | spy.reset(); 47 | }); 48 | 49 | describe('exec()', function () { 50 | let execMocked: typeof exec; 51 | 52 | before(async function () { 53 | const { exec } = await spy.mockedImport(); 54 | execMocked = exec; 55 | }); 56 | 57 | afterEach(function () { 58 | delete process.env['INPUT_THIS_IS_TEST']; 59 | delete process.env['WOOOO_THIS_IS_TEST']; 60 | }); 61 | 62 | it('returns stdout of given command execution', async function () { 63 | const out = await execMocked('test', ['--foo', '-b', 'piyo']); 64 | A.equal(out, 'this is stdout'); 65 | const [cmd, args] = spy.called; 66 | A.equal(cmd, 'test'); 67 | A.deepEqual(args, ['--foo', '-b', 'piyo']); 68 | }); 69 | 70 | it('throws an error when command fails', async function () { 71 | spy.exitCode = 1; 72 | await A.rejects(() => execMocked('test', []), { 73 | message: /exited non-zero status 1: this is stderr/, 74 | }); 75 | }); 76 | 77 | it('sets cwd', async function () { 78 | const cwd = '/path/to/cwd'; 79 | await execMocked('test', [], { cwd }); 80 | const [, , opts] = spy.called; 81 | A.equal(opts.cwd, cwd); 82 | }); 83 | 84 | it('sets env', async function () { 85 | const v = 'this is env var'; 86 | await execMocked('test', [], { env: { THIS_IS_TEST: v } }); 87 | const [, , opts] = spy.called; 88 | A.equal(opts.env['THIS_IS_TEST'], v); 89 | }); 90 | 91 | it('propagates outer env', async function () { 92 | process.env['WOOOO_THIS_IS_TEST'] = 'hello'; 93 | await execMocked('test', []); 94 | const [, , opts] = spy.called; 95 | A.equal(opts.env['WOOOO_THIS_IS_TEST'], 'hello'); 96 | }); 97 | 98 | it('filters input env vars', async function () { 99 | process.env['INPUT_THIS_IS_TEST'] = 'hello'; 100 | await execMocked('test', []); 101 | const [, , opts] = spy.called; 102 | A.equal(opts.env['INPUT_THIS_IS_TEST'], undefined); 103 | }); 104 | }); 105 | 106 | describe('unzip()', function () { 107 | it('runs `unzip` command with given working directory', async function () { 108 | delete process.env['RUNNER_DEBUG']; 109 | const { unzip } = await spy.mockedImport(); 110 | 111 | const file = '/path/to/file.zip'; 112 | const cwd = '/path/to/cwd'; 113 | await unzip(file, cwd); 114 | const [cmd, args, opts] = spy.called; 115 | A.equal(cmd, 'unzip'); 116 | A.deepEqual(args, ['-q', file]); 117 | A.equal(opts?.cwd, cwd); 118 | }); 119 | 120 | it('removes `-q` option when RUNNER_DEBUG environment variable is set', async function () { 121 | process.env['RUNNER_DEBUG'] = 'true'; 122 | const { unzip } = await spy.mockedImport(); 123 | 124 | const file = '/path/to/file.zip'; 125 | const cwd = '/path/to/cwd'; 126 | await unzip(file, cwd); 127 | const [cmd, args, opts] = spy.called; 128 | A.equal(cmd, 'unzip'); 129 | A.deepEqual(args, [file]); 130 | A.equal(opts?.cwd, cwd); 131 | }); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /test/validate.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | import { strict as A } from 'node:assert'; 3 | import { fileURLToPath } from 'node:url'; 4 | import process from 'node:process'; 5 | import { validateInstallation } from '../src/validate.js'; 6 | import type { Installed, ExeName } from '../src/install.js'; 7 | 8 | const FILENAME = fileURLToPath(import.meta.url); 9 | const DIRNAME = path.dirname(FILENAME); 10 | 11 | function getFakedInstallation(): Installed { 12 | // Use node executable instead of Vim or Neovim binaries 13 | const fullPath = process.argv[0]; 14 | const executable = path.basename(fullPath) as ExeName; 15 | const binDir = path.dirname(fullPath); 16 | return { executable, binDir }; 17 | } 18 | 19 | describe('validateInstallation()', function () { 20 | it('does nothing when correct installation is passed', async function () { 21 | const installed = getFakedInstallation(); 22 | await validateInstallation(installed); // Check no exception 23 | }); 24 | 25 | it("throws an error when 'bin' directory does not exist", async function () { 26 | const installed = { ...getFakedInstallation(), binDir: '/path/to/somewhere/not/exist' }; 27 | await A.rejects(() => validateInstallation(installed), /Could not stat installed directory/); 28 | }); 29 | 30 | it("throws an error when 'bin' directory is actually a file", async function () { 31 | const installed = { ...getFakedInstallation(), binDir: FILENAME }; 32 | await A.rejects(() => validateInstallation(installed), /is not a directory for executable/); 33 | }); 34 | 35 | it("throws an error when 'executable' file does not exist in 'bin' directory", async function () { 36 | const executable = 'this-file-does-not-exist-probably' as ExeName; 37 | const installed = { binDir: DIRNAME, executable }; 38 | await A.rejects(() => validateInstallation(installed), /Could not access the installed executable/); 39 | }); 40 | 41 | it("throws an error when file specified with 'executable' is actually not executable", async function () { 42 | // This file exists but not executable 43 | const executable = path.basename(FILENAME) as ExeName; 44 | const installed = { binDir: DIRNAME, executable }; 45 | await A.rejects(() => validateInstallation(installed), /Could not access the installed executable/); 46 | }); 47 | 48 | it('throws an error when getting version from executable failed', async function () { 49 | // prepare-release.sh exists and executable but does not support --version option 50 | const binDir = path.join(path.dirname(DIRNAME), 'scripts'); 51 | const executable = 'prepare-release.sh' as ExeName; 52 | const installed = { executable, binDir }; 53 | await A.rejects(() => validateInstallation(installed), /Could not get version from executable/); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/vim.ts: -------------------------------------------------------------------------------- 1 | import { strict as A } from 'node:assert'; 2 | import * as path from 'node:path'; 3 | import process from 'node:process'; 4 | import { installVimOnWindows, detectLatestWindowsReleaseTag, versionIsOlderThan, type buildVim } from '../src/vim.js'; 5 | import { importFetchMocked, ExecStub } from './helper.js'; 6 | 7 | describe('detectLatestWindowsReleaseTag()', function () { 8 | it('detects the latest release from redirect URL', async function () { 9 | const tag = await detectLatestWindowsReleaseTag(); 10 | const re = /^v\d+\.\d+\.\d{4}$/; 11 | A.ok(re.test(tag), `'${tag}' did not match to ${re}`); 12 | }); 13 | 14 | context('with mocking fetch()', function () { 15 | let detectLatestWindowsReleaseTagMocked: typeof detectLatestWindowsReleaseTag; 16 | 17 | before(async function () { 18 | const { detectLatestWindowsReleaseTag } = await importFetchMocked('../src/vim.js'); 19 | detectLatestWindowsReleaseTagMocked = detectLatestWindowsReleaseTag; 20 | }); 21 | 22 | it('throws an error when response is other than 302', async function () { 23 | await A.rejects( 24 | () => detectLatestWindowsReleaseTagMocked(), 25 | /Expected status 302 \(Redirect\) but got 404/, 26 | ); 27 | }); 28 | }); 29 | }); 30 | 31 | describe('installVimOnWindows()', function () { 32 | it('throws an error when the specified version does not exist', async function () { 33 | await A.rejects( 34 | () => installVimOnWindows('v0.1.2', 'v0.1.2'), 35 | /^Error: Could not download and unarchive asset/, 36 | ); 37 | }); 38 | 39 | context('with mocking fetch()', function () { 40 | let installVimOnWindowsMocked: typeof installVimOnWindows; 41 | 42 | before(async function () { 43 | const { installVimOnWindows } = await importFetchMocked('../src/vim.js'); 44 | installVimOnWindowsMocked = installVimOnWindows; 45 | }); 46 | 47 | it('throws an error when receiving unsuccessful response', async function () { 48 | await A.rejects( 49 | () => installVimOnWindowsMocked('nightly', 'nightly'), 50 | /Downloading asset failed: Not found for dummy/, 51 | ); 52 | }); 53 | }); 54 | }); 55 | 56 | describe('buildVim()', function () { 57 | const stub = new ExecStub(); 58 | let buildVimMocked: typeof buildVim; 59 | const savedXcode11Env = process.env['XCODE_11_DEVELOPER_DIR']; 60 | 61 | before(async function () { 62 | const { buildVim } = await stub.importWithMock('../src/vim.js'); 63 | buildVimMocked = buildVim; 64 | process.env['XCODE_11_DEVELOPER_DIR'] = './'; 65 | }); 66 | 67 | after(function () { 68 | process.env['XCODE_11_DEVELOPER_DIR'] = savedXcode11Env; 69 | }); 70 | 71 | afterEach(function () { 72 | stub.reset(); 73 | }); 74 | 75 | it('builds nightly Vim from source', async function () { 76 | const installed = await buildVimMocked('nightly', 'linux', null); 77 | A.equal(installed.executable, 'vim'); 78 | A.ok(installed.binDir.endsWith('bin'), installed.binDir); 79 | A.ok(stub.called.length > 0); 80 | 81 | const [cmd, args] = stub.called[0]; 82 | A.equal(cmd, 'git'); 83 | A.equal(args[0], 'clone'); 84 | A.equal(args[args.length - 2], 'https://github.com/vim/vim'); 85 | // Nightly uses HEAD. It means tags are unnecessary 86 | A.equal(args[args.length - 3], '--no-tags'); 87 | 88 | A.equal(stub.called[1][0], './configure'); 89 | const configurePrefix = stub.called[1][1][0]; // --prefix=installDir 90 | A.equal(`--prefix=${path.dirname(installed.binDir)}`, configurePrefix); 91 | }); 92 | 93 | it('builds recent Vim from source', async function () { 94 | const version = 'v8.2.2424'; 95 | const installed = await buildVimMocked(version, 'linux', null); 96 | A.equal(installed.executable, 'vim'); 97 | A.ok(installed.binDir.endsWith('bin'), installed.binDir); 98 | A.ok(stub.called.length > 0); 99 | 100 | const [cmd, args] = stub.called[0]; 101 | A.equal(cmd, 'git'); 102 | A.equal(args[0], 'clone'); 103 | A.equal(args[args.length - 2], 'https://github.com/vim/vim'); 104 | // Specify tag name for cloning specific version 105 | A.equal(args[args.length - 4], '--branch'); 106 | A.equal(args[args.length - 3], version); 107 | 108 | A.equal(stub.called[1][0], './configure'); 109 | const configurePrefix = stub.called[1][1][0]; // --prefix=installDir 110 | A.equal(`--prefix=${path.dirname(installed.binDir)}`, configurePrefix); 111 | }); 112 | 113 | it('builds older Vim from source on macOS', async function () { 114 | const version = 'v8.2.0000'; 115 | await buildVimMocked(version, 'macos', null); 116 | 117 | // For older Vim (before 8.2.1119), Xcode11 is necessary to build 118 | // Check `./configure`, `make` and `make install` are run with Xcode11 119 | for (let i = 1; i < 4; i++) { 120 | const opts = stub.called[i][2]; 121 | A.ok(opts); 122 | A.ok('env' in opts); 123 | const env = opts['env']; 124 | A.ok('DEVELOPER_DIR' in env); 125 | } 126 | }); 127 | 128 | it('builds Vim from source with specified configure arguments', async function () { 129 | const version = 'v8.2.2424'; 130 | const installed = await buildVimMocked( 131 | version, 132 | 'linux', 133 | '--with-features=huge --enable-fail-if-missing --disable-nls', 134 | ); 135 | 136 | const [cmd, args] = stub.called[1]; 137 | A.equal(cmd, './configure'); 138 | const expected = [ 139 | `--prefix=${path.dirname(installed.binDir)}`, 140 | '--with-features=huge', 141 | '--enable-fail-if-missing', 142 | '--disable-nls', 143 | ]; 144 | A.deepEqual(args, expected); 145 | }); 146 | }); 147 | 148 | describe('versionIsOlderThan()', function () { 149 | const testCases: [string, boolean][] = [ 150 | // Equal 151 | ['v8.2.1119', false], 152 | // Newer 153 | ['v8.2.1120', false], 154 | ['v8.3.0000', false], 155 | ['v8.3.1234', false], 156 | ['v8.3.0123', false], 157 | ['v9.0.0000', false], 158 | // Older 159 | ['v8.2.1118', true], 160 | ['v8.2.0000', true], 161 | ['v8.1.2000', true], 162 | ['v8.1.1000', true], 163 | ['v8.0.2000', true], 164 | ['v7.3.2000', true], 165 | ['v7.2', true], 166 | ['v6.4', true], 167 | // Invalid 168 | ['8.2.1119', false], // 'v' prefix not found 169 | ['8.2', false], // newer than v7 but patch version does not exist 170 | ]; 171 | 172 | for (const tc of testCases) { 173 | const [v, expected] = tc; 174 | 175 | it(`${v} is ${expected ? 'older than' : 'equal or newer than'} 8.2.1119`, function () { 176 | A.equal(versionIsOlderThan(v, 8, 2, 1119), expected); 177 | }); 178 | } 179 | }); 180 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "nodenext", 5 | "moduleResolution": "nodenext", 6 | "noEmit": true, 7 | "allowJs": true 8 | }, 9 | "files": [ 10 | "eslint.config.mjs" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "node16", 4 | "moduleResolution": "node16", 5 | "lib": [ 6 | "es2022" 7 | ], 8 | "preserveConstEnums": true, 9 | "noImplicitAny": true, 10 | "noImplicitReturns": true, 11 | "noImplicitThis": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noEmitOnError": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "noPropertyAccessFromIndexSignature": true, 17 | "strict": true, 18 | "target": "es2022", 19 | "sourceMap": true, 20 | "esModuleInterop": true 21 | }, 22 | "files": [ 23 | "src/config.ts", 24 | "src/shell.ts", 25 | "src/validate.ts", 26 | "src/vim.ts", 27 | "src/neovim.ts", 28 | "src/system.ts", 29 | "src/install.ts", 30 | "src/install_linux.ts", 31 | "src/install_macos.ts", 32 | "src/install_windows.ts", 33 | "src/index.ts", 34 | "test/config.ts", 35 | "test/validate.ts", 36 | "test/vim.ts", 37 | "test/neovim.ts", 38 | "test/shell.ts", 39 | "test/install_macos.ts", 40 | "test/install_linux.ts", 41 | "test/helper.ts", 42 | "scripts/post_action_check.ts" 43 | ] 44 | } 45 | --------------------------------------------------------------------------------