├── .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 |
--------------------------------------------------------------------------------