├── test ├── testdata │ ├── validate │ │ ├── dummy_file │ │ ├── .gitignore │ │ ├── vim_dir_empty │ │ │ └── .gitkeep │ │ ├── vim_dir_nvim │ │ │ └── runtime │ │ │ │ └── .gitkeep │ │ ├── vim_dir_vimver │ │ │ └── vim91 │ │ │ │ └── .gitkeep │ │ ├── vim_dir_runtime │ │ │ └── runtime │ │ │ │ └── .gitkeep │ │ ├── dummy.exe │ │ ├── dummy_non_version.bash │ │ ├── dummy_non_version.exe │ │ ├── dummy.bash │ │ ├── dummy_non_version.c │ │ └── dummy.c │ └── vimdir │ │ ├── empty │ │ └── .gitkeep │ │ ├── vim_ver │ │ └── vim91 │ │ │ └── .gitkeep │ │ └── vim_runtime │ │ └── runtime │ │ └── .gitkeep ├── install_linux.ts ├── helper.ts ├── system.ts ├── validate.ts ├── install_macos.ts ├── shell.ts ├── config.ts ├── vim.ts └── neovim.ts ├── .husky └── pre-push ├── .gitignore ├── .yamllint.yml ├── .prettierrc.json ├── tsconfig.eslint.json ├── .github └── workflows │ ├── codeql-analysis.yml │ ├── released.yml │ └── ci.yml ├── src ├── install.ts ├── index.ts ├── install_windows.ts ├── install_linux.ts ├── shell.ts ├── config.ts ├── system.ts ├── validate.ts ├── install_macos.ts ├── vim.ts └── neovim.ts ├── LICENSE.txt ├── tsconfig.json ├── action.yml ├── CONTRIBUTING.md ├── package.json ├── scripts ├── prepare-release.sh └── post_action_check.ts ├── eslint.config.mjs ├── README.md └── CHANGELOG.md /test/testdata/validate/dummy_file: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/testdata/vimdir/empty/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/testdata/vimdir/vim_ver/vim91/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/testdata/validate/.gitignore: -------------------------------------------------------------------------------- 1 | /*.obj 2 | -------------------------------------------------------------------------------- /test/testdata/validate/vim_dir_empty/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/testdata/validate/vim_dir_nvim/runtime/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/testdata/validate/vim_dir_vimver/vim91/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/testdata/vimdir/vim_runtime/runtime/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/testdata/validate/vim_dir_runtime/runtime/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | npx concurrently -c auto npm:lint npm:test 2 | -------------------------------------------------------------------------------- /test/testdata/validate/dummy.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhysd/action-setup-vim/HEAD/test/testdata/validate/dummy.exe -------------------------------------------------------------------------------- /test/testdata/validate/dummy_non_version.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo 'This script does not support --version' 4 | exit 1 5 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /test/testdata/validate/dummy_non_version.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhysd/action-setup-vim/HEAD/test/testdata/validate/dummy_non_version.exe -------------------------------------------------------------------------------- /test/testdata/validate/dummy.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ "$1" != "--version" ]]; then 4 | echo "--version is not specified in arguments: $*" 5 | exit 1 6 | fi 7 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "semi": true, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "printWidth": 120, 7 | "arrowParens": "avoid", 8 | "endOfLine": "auto" 9 | } 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/testdata/validate/dummy_non_version.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main(int argc, char *argv[]) { 5 | for (int i = 1; i < argc; i++) { 6 | if (strcmp(argv[i], "--version") == 0) { 7 | printf("--version is not supported\n"); 8 | return 1; 9 | } 10 | } 11 | return 0; 12 | } 13 | -------------------------------------------------------------------------------- /test/testdata/validate/dummy.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main(int argc, char *argv[]) { 5 | for (int i = 1; i < argc; i++) { 6 | if (strcmp(argv[i], "--version") == 0) { 7 | return 0; 8 | } 9 | } 10 | printf("--version arugment is not found in arguments (argc=%d)\n", argc); 11 | for (int i = 0; i < argc; i++) { 12 | printf(" %s\n", argv[i]); 13 | } 14 | return 1; 15 | } 16 | -------------------------------------------------------------------------------- /.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@v5 21 | - uses: actions/setup-node@v5 22 | with: 23 | node-version: '24' 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 | -------------------------------------------------------------------------------- /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 | readonly vimDir: string; 13 | } 14 | 15 | export function install(config: Config): Promise { 16 | core.debug(`Detected operating system: ${config.os}`); 17 | switch (config.os) { 18 | case 'linux': 19 | return installOnLinux(config); 20 | case 'macos': 21 | return installOnMacOs(config); 22 | case 'windows': 23 | return installOnWindows(config); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.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 | - 'scripts/post_action_check.ts' 10 | workflow_dispatch: 11 | 12 | jobs: 13 | validate: 14 | name: Validate release 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest, macos-15-intel, ubuntu-24.04-arm, windows-11-arm] 18 | neovim: [true, false] 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - uses: rhysd/action-setup-vim@v1 22 | id: vim 23 | with: 24 | neovim: ${{ matrix.neovim }} 25 | version: stable 26 | - uses: actions/checkout@v5 27 | - uses: actions/setup-node@v5 28 | with: 29 | node-version: '24' 30 | - run: npm ci 31 | - run: npm run build 32 | - name: Validate action result 33 | run: node ./scripts/post_action_check.js '${{ matrix.neovim }}' 'stable' '${{ toJSON(steps.vim.outputs) }}' 34 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "node20", 4 | "moduleResolution": "node16", 5 | "lib": [ 6 | "es2023" 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": "es2023", 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 | "test/system.ts", 43 | "scripts/post_action_check.ts" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /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 | core.info(`Extracted configuration: ${JSON.stringify(config, null, 2)}`); 10 | 11 | const installed = await install(config); 12 | await validateInstallation(installed, config.os); 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 | core.info(`Installed executable: ${fullPath}`); 20 | 21 | core.setOutput('vim-dir', installed.vimDir); 22 | core.info(`Installed $VIM directory: ${installed.vimDir}`); 23 | 24 | core.info(`Installation successfully done: ${JSON.stringify(installed, null, 2)}`); 25 | } 26 | 27 | main().catch((e: Error) => { 28 | if (e.stack) { 29 | core.debug(e.stack); 30 | } 31 | core.error(e.message); 32 | core.setFailed(e.message); 33 | }); 34 | -------------------------------------------------------------------------------- /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', config.arch); 22 | case 'nightly': 23 | return installNightlyVimOnWindows('nightly', config.arch); 24 | default: 25 | return installVimOnWindows(config.version, config.version, config.arch); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /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 | A.equal(installed.vimDir, '/usr/share/vim'); 33 | 34 | A.deepEqual(stub.called[0], ['sudo', ['apt-get', 'update', '-y', '-q']]); 35 | A.deepEqual(stub.called[1], [ 36 | 'sudo', 37 | ['apt-get', 'install', '-y', '--no-install-recommends', '-q', 'vim-gtk3'], 38 | ]); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /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 | vim-dir: 37 | description: > 38 | Absolute file path to the $VIM directory of the installation. Please see `:help $VIM` for 39 | more details. 40 | 41 | runs: 42 | using: 'node24' 43 | main: 'src/index.js' 44 | -------------------------------------------------------------------------------- /test/helper.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { Response } from 'node-fetch'; 4 | import esmock from 'esmock'; 5 | 6 | export const TESTDATA_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), 'testdata'); 7 | 8 | // Arguments of exec(): cmd: string, args: string[], options?: Options 9 | export type ExecArgs = [string, string[], { env: Record } | undefined]; 10 | export class ExecStub { 11 | called: ExecArgs[] = []; 12 | 13 | onCalled(args: ExecArgs): void { 14 | this.called.push(args); 15 | } 16 | 17 | reset(): void { 18 | this.called = []; 19 | } 20 | 21 | mockedExec(...args: ExecArgs): Promise { 22 | this.onCalled(args); 23 | return Promise.resolve(''); 24 | } 25 | 26 | importWithMock(path: string, otherMocks: object = {}): Promise { 27 | const exec = this.mockedExec.bind(this); 28 | return esmock(path, {}, { ...otherMocks, '../src/shell.js': { exec } }); 29 | } 30 | } 31 | 32 | export class FetchStub { 33 | fetchedUrls: string[] = []; 34 | 35 | reset(): void { 36 | this.fetchedUrls = []; 37 | } 38 | 39 | mockedFetch(url: string): Promise { 40 | this.fetchedUrls.push(url); 41 | const notFound = { status: 404, statusText: 'Not found for dummy' }; 42 | return Promise.resolve(new Response(`dummy response for ${url}`, notFound)); 43 | } 44 | 45 | importFetchMocked(path: string): Promise { 46 | return esmock(path, {}, { 'node-fetch': { default: this.mockedFetch.bind(this) } }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /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 | vimDir: '/usr/share/vim', 16 | }; 17 | } 18 | 19 | export async function install(config: Config): Promise { 20 | core.debug(`Installing ${config.neovim ? 'Neovim' : 'Vim'} version '${config.version}' on Linux`); 21 | if (config.neovim) { 22 | switch (config.version) { 23 | case 'stable': 24 | return downloadStableNeovim('linux', config.arch, config.token); 25 | case 'nightly': 26 | try { 27 | return await downloadNeovim(config.version, 'linux', config.arch); // await is necessary to catch error 28 | } catch (e) { 29 | const message = e instanceof Error ? e.message : String(e); 30 | core.warning( 31 | `Neovim download failure for nightly on Linux: ${message}. Falling back to installing Neovim by building it from source`, 32 | ); 33 | return buildNightlyNeovim('linux'); 34 | } 35 | default: 36 | return downloadNeovim(config.version, 'linux', config.arch); 37 | } 38 | } else { 39 | if (config.version === 'stable') { 40 | return installVimStable(); 41 | } else { 42 | return buildVim(config.version, config.os, config.configureArgs); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /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/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/system.ts: -------------------------------------------------------------------------------- 1 | import { tmpdir } from 'node:os'; 2 | import * as path from 'node:path'; 3 | import process from 'node:process'; 4 | import * as core from '@actions/core'; 5 | import { mkdirP, rmRF } from '@actions/io'; 6 | import { HttpsProxyAgent } from 'https-proxy-agent'; 7 | import { getProxyForUrl } from 'proxy-from-env'; 8 | 9 | export type Os = 'macos' | 'linux' | 'windows'; 10 | export type Arch = 'arm64' | 'x86_64' | 'arm32'; 11 | 12 | export function ensureError(err: unknown): Error { 13 | // TODO: Use `Error.isError` once ES2026 gets stable 14 | if (err instanceof Error) { 15 | return err; 16 | } 17 | // eslint-disable-next-line @typescript-eslint/restrict-template-expressions 18 | return new Error(`Unknown fatal error: ${err}`); 19 | } 20 | 21 | export class TmpDir { 22 | private constructor(public path: string) {} 23 | 24 | async cleanup(): Promise { 25 | try { 26 | await rmRF(this.path); 27 | } catch (err) { 28 | core.debug(`Could not remove the temporary directory ${this.path}: ${ensureError(err)}`); 29 | } 30 | } 31 | 32 | static async create(): Promise { 33 | const timestamp = new Date().getTime(); 34 | const dir = path.join(tmpdir(), `action-setup-vim-${timestamp}`); 35 | await mkdirP(dir); 36 | core.debug(`Created temporary directory ${dir}`); 37 | return new TmpDir(dir); 38 | } 39 | } 40 | 41 | export function getOs(): Os { 42 | switch (process.platform) { 43 | case 'darwin': 44 | return 'macos'; 45 | case 'linux': 46 | return 'linux'; 47 | case 'win32': 48 | return 'windows'; 49 | default: 50 | throw new Error(`Platform '${process.platform}' is not supported`); 51 | } 52 | } 53 | 54 | export function getArch(): Arch { 55 | switch (process.arch) { 56 | case 'arm': 57 | return 'arm32'; 58 | case 'arm64': 59 | return 'arm64'; 60 | case 'x64': 61 | return 'x86_64'; 62 | default: 63 | throw new Error(`CPU arch '${process.arch}' is not supported`); 64 | } 65 | } 66 | 67 | export function getSystemHttpsProxyAgent(url: string): HttpsProxyAgent | undefined { 68 | const u = getProxyForUrl(url); 69 | return u ? new HttpsProxyAgent(u) : undefined; 70 | } 71 | -------------------------------------------------------------------------------- /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 | ## Building 7 | 8 | Sources are written in [TypeScript][]. To compile them into JavaScript, 9 | 10 | ```sh 11 | npm run build 12 | ``` 13 | 14 | To clean the built files, 15 | 16 | ```sh 17 | npm run clean 18 | ``` 19 | 20 | ## Testing 21 | 22 | For testing validation for inputs and outputs, run unit tests: 23 | 24 | ```sh 25 | npm run test 26 | ``` 27 | 28 | Tests for installation logic are done in [CI workflows][ci] in E2E testing manner. All combinations 29 | of inputs are tested on the workflows triggered by `push` and `pull_request` events. 30 | 31 | After building and running `action-setup-vim` action, the workflow verifies the post conditions 32 | with [post_action_check.ts](./scripts/post_action_check.ts). 33 | 34 | To measure the code coverage run `npm run cov`. 35 | 36 | ## Linting 37 | 38 | In addition to type checking with TypeScript compiler, the following command checks the sources with 39 | [eslint][] and [pretteir][]. 40 | 41 | ```sh 42 | npm run lint 43 | ``` 44 | 45 | ## Node.js version 46 | 47 | Node.js version must be aligned with Node.js runtime in GitHub Actions. Check the version at 48 | `runs.using` in [action.yml](./action.yml) and use the same version for development. 49 | 50 | ## How to create a new release 51 | 52 | When releasing v1.2.3: 53 | 54 | 1. Make sure that `node --version` shows Node.js v24. 55 | 2. Run `$ bash scripts/prepare-release.sh v1.2.3`. It builds everything and prunes `node_modules` 56 | for removing all dev-dependencies. Then it copies built artifacts to `dev/v1` branch and makes 57 | a new commit and tag `v1.2.3`. Finally it rearrange `v1` and `v1.2` tags to point the new commit. 58 | 3. Check changes in the created commit with `git show`. 59 | 4. If ok, run `$ bash ./prepare-release.sh v1.2.3 --done` to apply the release to the remote. The 60 | script will push the branch and the new tag, then force-push the existing tags. 61 | 62 | ## Post release check 63 | 64 | [Post-release check workflow][post-release] runs checks against released `rhysd/action-setup-vim@v1` action. 65 | The workflow runs when modifying `CHANGELOG.md` and also runs on every Sunday 00:00 UTC. 66 | 67 | [repo]: https://github.com/rhysd/action-setup-vim 68 | [ts]: https://www.typescriptlang.org/ 69 | [ci]: https://github.com/rhysd/action-setup-vim/actions/workflows/ci.yml 70 | [eslint]: https://eslint.org/ 71 | [prettier]: https://prettier.io/ 72 | [post-release]: https://github.com/rhysd/action-setup-vim/actions?query=workflow%3A%22Post-release+check%22+branch%3Amaster 73 | -------------------------------------------------------------------------------- /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 . --pretty", 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": "npm run fix:eslint && npm run fix:prettier", 23 | "mocha": "mocha ./test", 24 | "test": "npm run build && npm run mocha", 25 | "c8": "c8 --reporter=lcov --reporter=text-summary npm run mocha", 26 | "open-cov": "open-cli ./coverage/lcov-report/index.html", 27 | "cov": "npm run build && npm run c8 && npm run open-cov", 28 | "clean": "git clean -fX", 29 | "prepare": "husky" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/rhysd/action-setup-vim.git" 34 | }, 35 | "keywords": [ 36 | "github", 37 | "action", 38 | "vim", 39 | "neovim", 40 | "text editor" 41 | ], 42 | "author": "rhysd ", 43 | "license": "MIT", 44 | "bugs": { 45 | "url": "https://github.com/rhysd/action-setup-vim/issues" 46 | }, 47 | "homepage": "https://github.com/rhysd/action-setup-vim#readme", 48 | "dependencies": { 49 | "@actions/core": "^1.11.1", 50 | "@actions/exec": "^1.1.1", 51 | "@actions/github": "^6.0.1", 52 | "@actions/io": "^2.0.0", 53 | "https-proxy-agent": "^7.0.6", 54 | "node-fetch": "^3.3.2", 55 | "proxy-from-env": "^1.1.0", 56 | "shlex": "^3.0.0" 57 | }, 58 | "devDependencies": { 59 | "@types/eslint": "^9.6.1", 60 | "@types/mocha": "^10.0.10", 61 | "@types/node": "^24", 62 | "@types/proxy-from-env": "^1.0.4", 63 | "c8": "^10.1.3", 64 | "concurrently": "^9.2.1", 65 | "eslint": "^9.39.0", 66 | "eslint-plugin-mocha": "^11.2.0", 67 | "eslint-plugin-n": "^17.23.1", 68 | "esmock": "^2.7.3", 69 | "husky": "^9.1.7", 70 | "mocha": "^11.7.4", 71 | "open-cli": "^8.0.0", 72 | "prettier": "^3.6.2", 73 | "typescript": "^5.9.3", 74 | "typescript-eslint": "^8.46.2" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /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, type Os } from './system.js'; 7 | 8 | async function validateExecutable(binDir: string, executable: string, os: Os): Promise { 9 | try { 10 | const s = await fs.stat(binDir); 11 | if (!s.isDirectory()) { 12 | throw new Error(`Validation failed! '${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 '${binDir}': ${err.message}`); 17 | } 18 | core.debug(`Installed directory '${binDir}' was validated`); 19 | 20 | const path = join(binDir, executable); 21 | try { 22 | await fs.access(path, fsconsts.X_OK); 23 | } catch (e) { 24 | const err = ensureError(e); 25 | throw new Error(`Validation failed! Could not access the installed executable '${path}': ${err.message}`); 26 | } 27 | // `X_OK` check does not work on Windows. Additional check is necessary. 28 | if (os === 'windows' && !executable.endsWith('.exe') && !executable.endsWith('.EXE')) { 29 | throw new Error(`Validation failed! Installed binary is not an executable file: ${executable}`); 30 | } 31 | 32 | try { 33 | const ver = await exec(path, ['--version']); 34 | core.info(`Installed version:\n${ver}`); 35 | } catch (e) { 36 | const err = ensureError(e); 37 | throw new Error(`Validation failed! Could not get version from executable '${path}': ${err.message}`); 38 | } 39 | 40 | core.debug(`Installed executable '${path}' was validated`); 41 | } 42 | 43 | async function validateVimDir(path: string): Promise { 44 | let entries; 45 | try { 46 | entries = await fs.readdir(path); 47 | } catch (e) { 48 | throw new Error(`Validation failed! Could not read the installed $VIM directory ${path}: ${ensureError(e)}`); 49 | } 50 | 51 | const reVimRuntime = /^vim\d+$/; 52 | for (const entry of entries) { 53 | if (reVimRuntime.test(entry) || entry === 'runtime') { 54 | core.debug(`$VIM directory '${path}' was validated`); 55 | return; // OK 56 | } 57 | } 58 | 59 | throw new Error( 60 | `Validation failed! $VIM directory ${path} contains no $VIMRUNTIME directory: ${JSON.stringify(entries)}`, 61 | ); 62 | } 63 | 64 | export async function validateInstallation(installed: Installed, os: Os): Promise { 65 | core.debug(`Validating installation for ${os}: ${JSON.stringify(installed)}`); 66 | await validateExecutable(installed.binDir, installed.executable, os); 67 | await validateVimDir(installed.vimDir); 68 | core.debug('Installation was successfully validated'); 69 | } 70 | -------------------------------------------------------------------------------- /test/system.ts: -------------------------------------------------------------------------------- 1 | import { strict as A } from 'node:assert'; 2 | import process from 'node:process'; 3 | import fs from 'node:fs/promises'; 4 | import * as path from 'node:path'; 5 | import { getSystemHttpsProxyAgent, ensureError, getOs, getArch, TmpDir } from '../src/system.js'; 6 | 7 | describe('getSystemHttpsProxyAgent()', function () { 8 | let savedEnv: Record; 9 | 10 | before(function () { 11 | savedEnv = { ...process.env }; 12 | }); 13 | 14 | afterEach(function () { 15 | process.env = { ...savedEnv }; 16 | }); 17 | 18 | it('returns `undefined` when no proxy is configured', function () { 19 | process.env = {}; 20 | A.equal(getSystemHttpsProxyAgent('https://example.com'), undefined); 21 | }); 22 | 23 | it('returns HTTPS proxy agent when $https_proxy is configured', function () { 24 | process.env = { https_proxy: 'https://example.com:8088' }; 25 | A.ok(getSystemHttpsProxyAgent('https://example.com')); 26 | }); 27 | 28 | it('looks at $no_proxy configuration', function () { 29 | process.env = { no_proxy: '*' }; 30 | A.equal(getSystemHttpsProxyAgent('https://example.com'), undefined); 31 | }); 32 | }); 33 | 34 | describe('ensureError()', function () { 35 | it('passes through Error object', function () { 36 | const want = new Error('test'); 37 | const have = ensureError(want); 38 | A.equal(want, have); 39 | }); 40 | 41 | it('passes through custom error object', function () { 42 | class MyError extends Error {} 43 | const want = new MyError('test'); 44 | const have = ensureError(want); 45 | A.equal(want, have); 46 | }); 47 | 48 | it('wraps non-Error object as Error object', function () { 49 | const err = ensureError('this is test'); 50 | A.ok(err instanceof Error); 51 | A.equal(err.message, 'Unknown fatal error: this is test'); 52 | }); 53 | }); 54 | 55 | describe('getOs()', function () { 56 | it('returns OS name', function () { 57 | const o = getOs(); 58 | A.ok(o === 'macos' || o === 'linux' || o === 'windows', `${o}`); 59 | }); 60 | }); 61 | 62 | describe('getArch()', function () { 63 | it('returns architecture name', function () { 64 | const a = getArch(); 65 | A.ok(a === 'x86_64' || a === 'arm64' || a === 'arm32', `${a}`); 66 | }); 67 | }); 68 | 69 | describe('TmpDir', function () { 70 | it('creates a temporary directory on create()', async function () { 71 | const tmp = await TmpDir.create(); 72 | try { 73 | const stat = await fs.stat(tmp.path); 74 | A.ok(stat.isDirectory()); 75 | const name = path.basename(tmp.path); 76 | A.match(name, /^action-setup-vim-\d+$/); 77 | } finally { 78 | await tmp.cleanup(); 79 | } 80 | }); 81 | 82 | it('deletes a temporary directory on cleanup()', async function () { 83 | const tmp = await TmpDir.create(); 84 | await tmp.cleanup(); 85 | await A.rejects(() => fs.stat(tmp.path)); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /test/validate.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | import { strict as A } from 'node:assert'; 3 | import process from 'node:process'; 4 | import { validateInstallation } from '../src/validate.js'; 5 | import type { Installed, ExeName } from '../src/install.js'; 6 | import { TESTDATA_PATH } from './helper.js'; 7 | import { type Os, getOs } from '../src/system.js'; 8 | 9 | const TEST_DIR = path.join(TESTDATA_PATH, 'validate'); 10 | const TEST_VIM_DIR = path.join(TEST_DIR, 'vim_dir_vimver'); 11 | 12 | function getFakedInstallation(os: Os): Installed { 13 | const executable = (os === 'windows' ? 'dummy.exe' : 'dummy.bash') as ExeName; 14 | return { executable, binDir: TEST_DIR, vimDir: TEST_VIM_DIR }; 15 | } 16 | 17 | describe('validateInstallation()', function () { 18 | const os = getOs(); 19 | it('does nothing when correct installation is passed', async function () { 20 | await validateInstallation(getFakedInstallation(os), os); // Check no exception 21 | }); 22 | 23 | it("throws an error when 'bin' directory does not exist", async function () { 24 | const installed = { ...getFakedInstallation(os), binDir: '/path/to/somewhere/not/exist' }; 25 | await A.rejects(() => validateInstallation(installed, os), /Could not stat installed directory/); 26 | }); 27 | 28 | it("throws an error when 'bin' directory is actually a file", async function () { 29 | const installed = { ...getFakedInstallation(os), binDir: path.join(TEST_DIR, 'dummy_file') }; 30 | await A.rejects(() => validateInstallation(installed, os), /is not a directory for executable/); 31 | }); 32 | 33 | it("throws an error when 'executable' file does not exist in 'bin' directory", async function () { 34 | const executable = 'this-file-does-not-exist-probably' as ExeName; 35 | const installed = { ...getFakedInstallation(os), executable }; 36 | await A.rejects( 37 | () => validateInstallation(installed, os), 38 | /Could not access the installed executable|Installed binary is not an executable file/, 39 | ); 40 | }); 41 | 42 | it('throws an error when the executable file is actually not executable', async function () { 43 | // This file exists but not executable 44 | const installed = { ...getFakedInstallation(os), executable: 'dummy_file' as ExeName }; 45 | await A.rejects( 46 | () => validateInstallation(installed, os), 47 | /Could not access the installed executable|Installed binary is not an executable file/, 48 | ); 49 | // Check .exe or .EXE extensions are necessary instead of executable permission on Windows 50 | installed.executable = 'dummy.bash' as ExeName; 51 | await A.rejects(() => validateInstallation(installed, 'windows'), /Installed binary is not an executable file/); 52 | }); 53 | 54 | it('throws an error when getting version from executable failed', async function () { 55 | const executable = ( 56 | process.platform === 'win32' ? 'dummy_non_version.exe' : 'dummy_non_version.bash' 57 | ) as ExeName; 58 | const installed = { ...getFakedInstallation(os), executable }; 59 | await A.rejects(() => validateInstallation(installed, os), /Could not get version from executable/); 60 | }); 61 | 62 | it('does nothing when correct $VIM directory is found', async function () { 63 | for (const dir of ['vim_dir_vimver', 'vim_dir_runtime', 'vim_dir_nvim']) { 64 | const installed = { ...getFakedInstallation(os), vimDir: path.join(TEST_DIR, dir) }; 65 | await validateInstallation(installed, os); 66 | } 67 | }); 68 | 69 | it('throws an error when $VIM directory does not exist', async function () { 70 | const installed = { ...getFakedInstallation(os), vimDir: path.join(TEST_DIR, 'this-dir-doesnt-exist') }; 71 | await A.rejects( 72 | () => validateInstallation(installed, os), 73 | /Validation failed! Could not read the installed \$VIM directory /, 74 | ); 75 | }); 76 | 77 | it('throws an error when $VIM directory does not contain $VIMRUNTIME directory', async function () { 78 | const installed = { ...getFakedInstallation(os), vimDir: path.join(TEST_DIR, 'vim_dir_empty') }; 79 | await A.rejects(() => validateInstallation(installed, os), /contains no \$VIMRUNTIME directory/); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /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 | let dummyRealPath: string; 10 | 11 | before(async function () { 12 | const { install } = await stub.importWithMock('../src/install_macos.js', { 13 | 'fs/promises': { 14 | realpath(path: string): Promise { 15 | // When using realpath() for $VIM directory, just pass through the path. Otherwise 16 | // it is used for resolving Python symlinks so return the dummy path. 17 | const ret = path.endsWith('/vim') || path.endsWith('/nvim') ? path : dummyRealPath; 18 | return Promise.resolve(ret); 19 | }, 20 | }, 21 | '@actions/io': { 22 | rmRF: (): Promise => Promise.resolve(), 23 | }, 24 | }); 25 | installMocked = install; 26 | }); 27 | 28 | beforeEach(function () { 29 | stub.reset(); 30 | dummyRealPath = '/Library/Frameworks/Python.framework/Versions/dummy'; 31 | }); 32 | 33 | it('installs stable Neovim from Homebrew', async function () { 34 | const config: Config = { 35 | version: 'stable', 36 | neovim: true, 37 | os: 'macos', 38 | arch: 'arm64', 39 | configureArgs: null, 40 | token: null, 41 | }; 42 | 43 | const installed = await installMocked(config); 44 | A.equal(installed.executable, 'nvim'); 45 | A.equal(installed.binDir, '/opt/homebrew/bin'); 46 | A.equal(installed.vimDir, '/opt/homebrew/opt/neovim/share/nvim'); 47 | 48 | A.deepEqual(stub.called[0], ['brew', ['update', '--quiet']]); 49 | A.deepEqual(stub.called[1], ['brew', ['install', 'neovim', '--quiet']]); 50 | }); 51 | 52 | it('installs stable Vim from Homebrew', async function () { 53 | const config: Config = { 54 | version: 'stable', 55 | neovim: false, 56 | os: 'macos', 57 | arch: 'arm64', 58 | configureArgs: null, 59 | token: null, 60 | }; 61 | 62 | const installed = await installMocked(config); 63 | A.equal(installed.executable, 'vim'); 64 | A.equal(installed.binDir, '/opt/homebrew/bin'); 65 | A.equal(installed.vimDir, '/opt/homebrew/opt/macvim/MacVim.app/Contents/Resources/vim'); 66 | 67 | A.deepEqual(stub.called[0], ['brew', ['update', '--quiet']]); 68 | A.deepEqual(stub.called[1], ['brew', ['install', 'macvim', '--quiet']]); 69 | A.equal(stub.called.length, 2); 70 | }); 71 | 72 | it('avoids python package conflict on x86_64 (#52)', async function () { 73 | const config: Config = { 74 | version: 'stable', 75 | neovim: false, 76 | os: 'macos', 77 | arch: 'x86_64', 78 | configureArgs: null, 79 | token: null, 80 | }; 81 | 82 | await installMocked(config); 83 | 84 | A.deepEqual(stub.called[0], ['brew', ['unlink', 'python@3', '--quiet']]); 85 | A.deepEqual(stub.called[1], ['brew', ['link', 'python@3', '--quiet', '--overwrite']]); 86 | A.deepEqual(stub.called[2], ['brew', ['update', '--quiet']]); 87 | A.deepEqual(stub.called[3], ['brew', ['install', 'macvim', '--quiet']]); 88 | A.equal(stub.called.length, 4); 89 | }); 90 | 91 | it('does not do package conflict workaround when executables are not linked to Python.Framework', async function () { 92 | dummyRealPath = '/usr/local/opt/python@3/bin/dummy'; 93 | const config: Config = { 94 | version: 'stable', 95 | neovim: false, 96 | os: 'macos', 97 | arch: 'x86_64', 98 | configureArgs: null, 99 | token: null, 100 | }; 101 | 102 | await installMocked(config); 103 | 104 | A.deepEqual(stub.called[0], ['brew', ['update', '--quiet']]); 105 | A.deepEqual(stub.called[1], ['brew', ['install', 'macvim', '--quiet']]); 106 | A.equal(stub.called.length, 2); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /.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-15-intel, ubuntu-24.04, ubuntu-24.04-arm, windows-11-arm] 10 | version: [stable, nightly] 11 | neovim: [true, false] 12 | fail-fast: false 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - uses: actions/checkout@v5 16 | - uses: actions/setup-node@v5 17 | with: 18 | node-version: '24' 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 }}' '${{ toJSON(steps.vim.outputs) }}' 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@v5 40 | - uses: actions/setup-node@v5 41 | with: 42 | node-version: '24' 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' '${{ toJSON(steps.vim.outputs) }}' 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@v5 63 | - uses: actions/setup-node@v5 64 | with: 65 | node-version: '24' 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' '${{ toJSON(steps.neovim.outputs) }}' 76 | 77 | nvim-v0_10_3: 78 | name: Neovim v0.10.3 79 | runs-on: ubuntu-latest 80 | steps: 81 | - uses: actions/checkout@v5 82 | - uses: actions/setup-node@v5 83 | with: 84 | node-version: '24' 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' '${{ toJSON(steps.neovim.outputs) }}' 95 | 96 | unit-test: 97 | name: Check unit tests 98 | strategy: 99 | matrix: 100 | os: [ubuntu-latest, macos-latest, windows-latest] 101 | runs-on: ${{ matrix.os }} 102 | steps: 103 | - uses: actions/checkout@v5 104 | - uses: actions/setup-node@v5 105 | with: 106 | node-version: '24' 107 | cache: npm 108 | - run: npm ci 109 | - name: Run unit tests 110 | run: npm test 111 | env: 112 | GITHUB_TOKEN: ${{ github.token }} 113 | 114 | lint: 115 | name: Check lints 116 | runs-on: ubuntu-latest 117 | steps: 118 | - uses: actions/checkout@v5 119 | - uses: actions/setup-node@v5 120 | with: 121 | node-version: '24' 122 | cache: npm 123 | - run: npm ci 124 | - run: npm run build 125 | - run: npm run lint 126 | - uses: actions/setup-python@v6 127 | with: 128 | python-version: '3.x' 129 | - run: pip install yamllint 130 | - run: yamllint --strict .github/workflows 131 | - name: Check workflow files 132 | run: | 133 | bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) 134 | ./actionlint -color 135 | -------------------------------------------------------------------------------- /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 checks 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 | if ! git diff --quiet; then 38 | echo 'Working tree is dirty! Please ensure all changes are committed and working tree is clean' >&2 39 | exit 1 40 | fi 41 | if ! git diff --cached --quiet; then 42 | echo 'Git index is dirty! Please ensure all changes are committed and Git index is clean' >&2 43 | exit 1 44 | fi 45 | node_version="$(node --version)" 46 | if [[ ! "$node_version" =~ ^v24\.[0-9]+\.[0-9]+$ ]]; then 47 | echo "This script requires Node.js v24 but got '${node_version}'" 48 | exit 1 49 | fi 50 | 51 | current_branch="$(git symbolic-ref --short HEAD)" 52 | 53 | # Deploy release branch 54 | if [[ "$#" == 2 ]] && [[ "$2" == "--done" ]]; then 55 | echo "Deploying ${target_branch} branch and ${version}, ${minor_version}, ${major_version} tags to 'origin' remote" 56 | if [[ "$current_branch" != "${target_branch}" ]]; then 57 | echo "--done must be run in target branch '${target_branch}' but actually run in '${current_branch}'" >&2 58 | exit 1 59 | fi 60 | 61 | set -x 62 | git push origin "${target_branch}" 63 | git push origin "${version}" 64 | git push origin "${minor_version}" --force 65 | git push origin "${major_version}" --force 66 | # Remove copied prepare-release.sh in target branch 67 | rm -rf ./prepare-release.sh 68 | set +x 69 | 70 | echo "Done. Releases were pushed to 'origin' remote" 71 | exit 0 72 | fi 73 | 74 | if [[ "$current_branch" != "master" ]]; then 75 | echo 'Current branch is not master. Please move to master before running this script' >&2 76 | exit 1 77 | fi 78 | 79 | echo "Checking tests and eslint results" 80 | 81 | npm run test 82 | npm run lint 83 | 84 | echo "Releasing to ${target_branch} branch for ${version}... (minor=${minor_version}, major=${major_version})" 85 | 86 | set -x 87 | npm install 88 | npm run build 89 | npm test 90 | npm prune --production 91 | 92 | # Remove all type definitions from node_modules since @octokit/rest/index.d.ts is very big (1.3MB) 93 | find ./node_modules/ -name '*.d.ts' -exec rm '{}' \; 94 | 95 | # Remove coverage files 96 | rm -rf ./node_modules/.cache ./.nyc_output ./coverage 97 | 98 | rm -rf .release 99 | mkdir -p .release 100 | 101 | cp action.yml src/*.js package.json package-lock.json ./scripts/prepare-release.sh .release/ 102 | cp -R node_modules .release/node_modules 103 | 104 | sha="$(git rev-parse HEAD)" 105 | 106 | git checkout "${target_branch}" 107 | git pull 108 | if [ -d node_modules ]; then 109 | git rm -rf node_modules || true 110 | rm -rf node_modules # remove node_modules/.cache 111 | fi 112 | mkdir -p src 113 | 114 | mv .release/action.yml . 115 | mv .release/*.js ./src/ 116 | mv .release/*.json . 117 | mv .release/node_modules . 118 | # Copy release script to release branch for --done 119 | mv .release/prepare-release.sh . 120 | 121 | git add action.yml ./src/*.js package.json package-lock.json node_modules 122 | git commit -m "Release ${version} at ${sha}" 123 | 124 | git tag -d "$major_version" || true 125 | git tag "$major_version" 126 | git tag -d "$minor_version" || true 127 | git tag "$minor_version" 128 | git tag "$version" 129 | set +x 130 | 131 | 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'" 132 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/install_macos.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import * as core from '@actions/core'; 3 | import { rmRF } from '@actions/io'; 4 | import type { Installed } from './install.js'; 5 | import type { Config } from './config.js'; 6 | import { type Arch, ensureError } from './system.js'; 7 | import { exec } from './shell.js'; 8 | import { buildVim } from './vim.js'; 9 | import { buildNightlyNeovim, downloadNeovim } from './neovim.js'; 10 | 11 | function homebrewPrefixDir(arch: Arch): string { 12 | switch (arch) { 13 | case 'arm64': 14 | return '/opt/homebrew'; 15 | case 'x86_64': 16 | return '/usr/local'; 17 | default: 18 | throw new Error(`CPU arch ${arch} is not supported by Homebrew`); 19 | } 20 | } 21 | 22 | async function removePreinstalledPythonSymlink(bin: string): Promise { 23 | const path = `/usr/local/bin/${bin}`; 24 | 25 | let realpath; 26 | try { 27 | realpath = await fs.realpath(path); 28 | } catch (err) { 29 | core.debug(`Cancel removing ${path} symlink: ${ensureError(err)}`); 30 | return false; 31 | } 32 | 33 | if (!realpath.startsWith('/Library/Frameworks/Python.framework/Versions')) { 34 | core.debug(`Symlink ${path} is not linked to Python.Framework: ${realpath}`); 35 | return false; 36 | } 37 | 38 | core.debug(`Removing ${path} symlinked to ${realpath} for workaround of #52`); 39 | await rmRF(path); 40 | return true; 41 | } 42 | 43 | // `macvim` now depends on `python@3.14`. Now installing `macvim` fails due to link error on `python@3.14` installation 44 | // on macos-15-intel runner (#52). The link error is caused by executable conflicts in /usr/local/bin. GitHub installs 45 | // Python using the official Python installer. The installer puts symlinks in /usr/local/bin. Since symlinks not managed 46 | // by Homebrew are already there, Homebrew cannot create its symlinks. 47 | // We avoid this issue by forcing to overwrite the installer's symlinks by `brew link python@3` before installing the 48 | // MacVim's `python@3.14` dependency so that Homebrew can make the python@3.13's symlinks without confusion. 49 | // 50 | // - https://github.com/rhysd/action-setup-vim/issues/52 51 | // - https://github.com/Homebrew/homebrew-core/pull/248952 52 | // - https://github.com/actions/runner-images/issues/9966 53 | async function ensureHomebrewPythonIsLinked(arch: Arch): Promise { 54 | if (arch !== 'x86_64') { 55 | return; 56 | } 57 | 58 | let anyRemoved = false; 59 | for (const bin of ['idle3', 'pip3', 'pydoc3', 'python3', 'python3-config']) { 60 | const removed = await removePreinstalledPythonSymlink(bin); 61 | anyRemoved ||= removed; 62 | } 63 | if (!anyRemoved) { 64 | return; 65 | } 66 | 67 | // Create the removed symlinks again by Homebrew so that Homebrew is no longer confused by them 68 | core.info("Ensure linking Homebrew's python@3 package to avoid conflicts in /usr/local/bin (#52)"); 69 | await exec('brew', ['unlink', 'python@3', '--quiet']); 70 | await exec('brew', ['link', 'python@3', '--quiet', '--overwrite']); 71 | } 72 | 73 | async function brewInstall(pkg: string): Promise { 74 | await exec('brew', ['update', '--quiet']); 75 | await exec('brew', ['install', pkg, '--quiet']); 76 | } 77 | 78 | async function installVimStable(arch: Arch): Promise { 79 | core.debug('Installing stable Vim on macOS using Homebrew'); 80 | await ensureHomebrewPythonIsLinked(arch); 81 | await brewInstall('macvim'); 82 | const prefix = homebrewPrefixDir(arch); 83 | return { 84 | executable: 'vim', 85 | binDir: prefix + '/bin', 86 | vimDir: await fs.realpath(prefix + '/opt/macvim/MacVim.app/Contents/Resources/vim'), 87 | }; 88 | } 89 | 90 | async function installNeovimStable(arch: Arch): Promise { 91 | core.debug('Installing stable Neovim on macOS using Homebrew'); 92 | await brewInstall('neovim'); 93 | const prefix = homebrewPrefixDir(arch); 94 | return { 95 | executable: 'nvim', 96 | binDir: prefix + '/bin', 97 | vimDir: await fs.realpath(prefix + '/opt/neovim/share/nvim'), 98 | }; 99 | } 100 | 101 | export async function install(config: Config): Promise { 102 | core.debug(`Installing ${config.neovim ? 'Neovim' : 'Vim'} ${config.version} version on macOS`); 103 | if (config.neovim) { 104 | switch (config.version) { 105 | case 'stable': 106 | return installNeovimStable(config.arch); 107 | case 'nightly': 108 | try { 109 | return await downloadNeovim(config.version, 'macos', config.arch); // await is necessary to catch error 110 | } catch (e) { 111 | const message = e instanceof Error ? e.message : String(e); 112 | core.warning( 113 | `Neovim download failure for nightly on macOS: ${message}. Falling back to installing Neovim by building it from source`, 114 | ); 115 | return buildNightlyNeovim('macos'); 116 | } 117 | default: 118 | return downloadNeovim(config.version, 'macos', config.arch); 119 | } 120 | } else { 121 | if (config.version === 'stable') { 122 | return installVimStable(config.arch); 123 | } else { 124 | return buildVim(config.version, config.os, config.configureArgs); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from '@eslint/js'; 4 | import { defineConfig } from 'eslint/config'; 5 | import ts from 'typescript-eslint'; 6 | import mocha from 'eslint-plugin-mocha'; 7 | import n from 'eslint-plugin-n'; 8 | 9 | export default defineConfig( 10 | eslint.configs.recommended, 11 | ts.configs.recommendedTypeChecked, 12 | n.configs['flat/recommended'], 13 | { 14 | languageOptions: { 15 | parserOptions: { 16 | projectService: true, 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 | 'no-console': 'error', 27 | '@typescript-eslint/explicit-function-return-type': 'error', 28 | '@typescript-eslint/no-floating-promises': 'error', 29 | '@typescript-eslint/no-unnecessary-type-arguments': 'error', 30 | '@typescript-eslint/no-non-null-assertion': 'error', 31 | '@typescript-eslint/no-empty-interface': 'error', 32 | '@typescript-eslint/restrict-plus-operands': 'error', 33 | '@typescript-eslint/no-extra-non-null-assertion': 'error', 34 | '@typescript-eslint/prefer-nullish-coalescing': 'error', 35 | '@typescript-eslint/prefer-optional-chain': 'error', 36 | '@typescript-eslint/prefer-includes': 'error', 37 | '@typescript-eslint/prefer-for-of': 'error', 38 | '@typescript-eslint/prefer-string-starts-ends-with': 'error', 39 | '@typescript-eslint/prefer-readonly': 'error', 40 | '@typescript-eslint/prefer-ts-expect-error': 'error', 41 | '@typescript-eslint/no-non-null-asserted-optional-chain': 'error', 42 | '@typescript-eslint/await-thenable': 'error', 43 | '@typescript-eslint/no-unnecessary-boolean-literal-compare': 'error', 44 | '@typescript-eslint/ban-ts-comment': [ 45 | 'error', 46 | { 47 | 'ts-ignore': true, 48 | 'ts-nocheck': true, 49 | }, 50 | ], 51 | '@typescript-eslint/naming-convention': [ 52 | 'error', 53 | { 54 | selector: 'default', 55 | format: ['camelCase', 'PascalCase', 'UPPER_CASE'], 56 | leadingUnderscore: 'allow', 57 | }, 58 | ], 59 | 'no-unused-vars': 'off', 60 | '@typescript-eslint/no-unused-vars': ['error', { caughtErrorsIgnorePattern: '^_' }], 61 | '@typescript-eslint/no-confusing-void-expression': 'error', 62 | '@typescript-eslint/non-nullable-type-assertion-style': 'error', 63 | 'no-return-await': 'off', 64 | '@typescript-eslint/return-await': ['error', 'in-try-catch'], 65 | '@typescript-eslint/no-invalid-void-type': 'error', 66 | '@typescript-eslint/prefer-as-const': 'error', 67 | '@typescript-eslint/consistent-indexed-object-style': 'error', 68 | '@typescript-eslint/no-base-to-string': 'error', 69 | '@typescript-eslint/switch-exhaustiveness-check': ['error', { considerDefaultExhaustiveForUnions: true }], 70 | '@typescript-eslint/no-deprecated': 'error', 71 | 'n/handle-callback-err': 'error', 72 | 'n/prefer-promises/fs': 'error', 73 | 'n/prefer-global/buffer': ['error', 'never'], 74 | 'n/prefer-global/process': ['error', 'never'], 75 | 'n/prefer-node-protocol': 'error', 76 | 'n/no-sync': 'error', 77 | }, 78 | }, 79 | { 80 | files: ['scripts/*.ts'], 81 | rules: { 82 | 'n/no-sync': 'off', 83 | 'no-console': 'off', 84 | }, 85 | }, 86 | { 87 | files: ['test/*.ts'], 88 | // The cast is workaround for https://github.com/lo1tuma/eslint-plugin-mocha/issues/392 89 | .../** @type {{recommended: import('eslint').Linter.Config}} */ (mocha.configs).recommended, 90 | }, 91 | { 92 | files: ['test/*.ts'], 93 | rules: { 94 | '@typescript-eslint/no-unsafe-return': 'off', 95 | '@typescript-eslint/restrict-template-expressions': 'off', 96 | '@typescript-eslint/no-unsafe-member-access': 'off', 97 | '@typescript-eslint/no-unsafe-assignment': 'off', 98 | '@typescript-eslint/no-explicit-any': 'off', 99 | '@typescript-eslint/no-unsafe-call': 'off', 100 | '@typescript-eslint/no-require-imports': 'off', 101 | '@typescript-eslint/naming-convention': 'off', 102 | 'mocha/no-setup-in-describe': 'off', 103 | 'mocha/no-hooks-for-single-case': 'off', 104 | 'mocha/max-top-level-suites': 'off', 105 | 'mocha/consistent-spacing-between-blocks': 'off', // Conflict with prettier 106 | 'mocha/no-exclusive-tests': 'error', 107 | 'mocha/no-pending-tests': 'error', 108 | 'mocha/no-top-level-hooks': 'error', 109 | 'mocha/consistent-interface': ['error', { interface: 'BDD' }], 110 | }, 111 | }, 112 | { 113 | files: ['eslint.config.mjs'], 114 | languageOptions: { 115 | parserOptions: { 116 | projectService: false, 117 | project: 'tsconfig.eslint.json', 118 | }, 119 | }, 120 | rules: { 121 | '@typescript-eslint/naming-convention': 'off', 122 | 'n/no-extraneous-import': 'off', 123 | }, 124 | }, 125 | ); 126 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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, readdirSync } 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 | interface Args { 13 | neovim: boolean; 14 | version: string; 15 | executable: string; 16 | vimdir: string; 17 | } 18 | 19 | function parseArgs(args: string[]): Args { 20 | if (args.length !== 5) { 21 | throw new Error('3 arguments must be set: `node ./scripts/post_action_check.js {neovim?} {version} {outputs}`'); 22 | } 23 | const neovim = args[2].toLowerCase() === 'true'; 24 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 25 | const outputs = JSON.parse(args[4]); 26 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access 27 | return { neovim, version: args[3], executable: outputs.executable, vimdir: outputs['vim-dir'] }; 28 | } 29 | 30 | // e.g. ~/vim-stable/vim91 31 | function getRuntimeDirOnWindows(version: string): string { 32 | const vimdir = path.join(homedir(), `vim-${version}`); 33 | for (const entry of readdirSync(vimdir)) { 34 | if (/^vim\d+$/.test(entry)) { 35 | return path.join(vimdir, entry); 36 | } 37 | } 38 | throw new Error(`vim{ver} directory for version ${version} is not found in $VIMDIR ${vimdir}`); 39 | } 40 | 41 | function expectedExecutable(neovim: boolean, ver: string): string { 42 | if (neovim) { 43 | switch (process.platform) { 44 | case 'darwin': 45 | if (ver === 'stable') { 46 | if (process.arch === 'arm64') { 47 | return '/opt/homebrew/bin/nvim'; 48 | } else { 49 | return '/usr/local/bin/nvim'; 50 | } 51 | } else { 52 | // nightly or specific version 53 | return path.join(homedir(), `nvim-${ver}/bin/nvim`); 54 | } 55 | case 'linux': 56 | return path.join(homedir(), `nvim-${ver}/bin/nvim`); 57 | case 'win32': 58 | return path.join(homedir(), `nvim-${ver}/bin/nvim.exe`); 59 | default: 60 | break; 61 | } 62 | } else { 63 | // vim 64 | switch (process.platform) { 65 | case 'darwin': 66 | if (ver === 'stable') { 67 | if (process.arch === 'arm64') { 68 | return '/opt/homebrew/bin/vim'; 69 | } else { 70 | return '/usr/local/bin/vim'; 71 | } 72 | } else { 73 | // nightly or specific version 74 | return path.join(homedir(), `vim-${ver}/bin/vim`); 75 | } 76 | case 'linux': 77 | if (ver === 'stable') { 78 | return '/usr/bin/vim'; 79 | } else { 80 | // nightly or specific version 81 | return path.join(homedir(), `vim-${ver}/bin/vim`); 82 | } 83 | case 'win32': 84 | return path.join(getRuntimeDirOnWindows(ver), 'vim.exe'); 85 | default: 86 | break; 87 | } 88 | } 89 | throw new Error(`Unexpected platform '${process.platform}'`); 90 | } 91 | 92 | function getVimVariable(variable: string, exe: string, neovim: boolean): string { 93 | const args = [`+put=${variable}|print|q!`]; 94 | if (neovim) { 95 | args.unshift('--headless', '--clean'); 96 | } else { 97 | args.unshift('--not-a-term', '-u', 'NONE', '-i', 'NONE', '-N', '-n', '-e', '-s', '--noplugin'); 98 | } 99 | const proc = spawnSync(exe, args, { timeout: 5000, encoding: 'utf-8' }); 100 | assert.equal(proc.error, undefined, `stderr=${proc.stderr}`); 101 | return proc.stdout.trim(); 102 | } 103 | 104 | function main(): void { 105 | log('Running with argv:', process.argv); 106 | 107 | const args = parseArgs(process.argv); 108 | log('Command line arguments:', args); 109 | 110 | const exe = expectedExecutable(args.neovim, args.version); 111 | log('Validating executable path. Expected executable:', exe); 112 | assert.ok(path.isAbsolute(exe)); 113 | assert.equal(exe, args.executable); 114 | assert.ok(existsSync(exe)); 115 | 116 | const bin = path.dirname(exe); 117 | log(`Validating '${bin}' is in $PATH`); 118 | assert.ok(process.env['PATH']); 119 | const pathSep = process.platform === 'win32' ? ';' : ':'; 120 | const paths = process.env['PATH'].split(pathSep); 121 | assert.ok(paths.includes(bin), `'${bin}' is not included in '${process.env['PATH']}'`); 122 | 123 | log('Validating executable'); 124 | const proc = spawnSync(exe, ['-N', '-c', 'quit'], { timeout: 5000, encoding: 'utf-8' }); 125 | assert.equal(proc.error, undefined); 126 | assert.equal(proc.status, 0, `stderr: ${proc.stderr}`); 127 | assert.equal(proc.signal, null, `stderr: ${proc.stderr}`); 128 | 129 | log('Validating version'); 130 | const ver = spawnSync(exe, ['--version'], { timeout: 5000, encoding: 'utf-8' }); 131 | assert.equal(ver.error, undefined); 132 | assert.equal(ver.status, 0, `stderr: ${ver.stderr}`); 133 | assert.equal(ver.signal, null, `stderr: ${ver.stderr}`); 134 | const stdout = ver.stdout.toString(); 135 | if (args.version !== 'stable' && args.version !== 'nightly') { 136 | if (args.neovim) { 137 | const l = `NVIM ${args.version}`; 138 | assert.ok(stdout.includes(l), `First line '${l}' should be included in stdout: ${stdout}`); 139 | } else { 140 | const m = args.version.match(/^v(\d+\.\d+)\.(\d+)$/); 141 | assert.ok(m); 142 | const major = m[1]; 143 | const patch = parseInt(m[2], 10); 144 | 145 | const l = `VIM - Vi IMproved ${major}`; 146 | assert.ok(stdout.includes(l), `First line '${l}' should be included in stdout: ${stdout}`); 147 | 148 | // assert.match is not available since it is experimental 149 | assert.ok( 150 | stdout.includes(`Included patches: 1-${patch}`), 151 | `Patch 1-${patch} should be included in stdout: ${stdout}`, 152 | ); 153 | } 154 | } else { 155 | const editorName = args.neovim ? 'NVIM' : 'VIM - Vi IMproved'; 156 | assert.ok(stdout.includes(editorName), `Editor name '${editorName}' should be included in stdout: ${stdout}`); 157 | } 158 | 159 | log('Validating $VIM directory', args.vimdir); 160 | assert.ok(path.isAbsolute(args.vimdir)); 161 | let expected = getVimVariable('$VIM', exe, args.neovim); 162 | if (process.platform === 'win32') { 163 | // Neovim mixes '\' and '/' in $VIM value (\path\to\nvim-nightly\share/nvim) 164 | expected = path.win32.normalize(expected); 165 | } 166 | assert.equal(expected, args.vimdir); 167 | 168 | log('OK'); 169 | } 170 | 171 | main(); 172 | -------------------------------------------------------------------------------- /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 { 5 | installVimOnWindows, 6 | detectLatestWindowsReleaseTag, 7 | versionIsOlderThan, 8 | getRuntimeDirInVimDir, 9 | type buildVim, 10 | } from '../src/vim.js'; 11 | import { ExecStub, FetchStub, TESTDATA_PATH } from './helper.js'; 12 | import { type Arch } from '../src/system.js'; 13 | 14 | describe('detectLatestWindowsReleaseTag()', function () { 15 | it('detects the latest release from redirect URL', async function () { 16 | const tag = await detectLatestWindowsReleaseTag(); 17 | const re = /^v\d+\.\d+\.\d{4}$/; 18 | A.ok(re.test(tag), `'${tag}' did not match to ${re}`); 19 | }); 20 | 21 | context('with mocking fetch()', function () { 22 | let detectLatestWindowsReleaseTagMocked: typeof detectLatestWindowsReleaseTag; 23 | 24 | before(async function () { 25 | const stub = new FetchStub(); 26 | const { detectLatestWindowsReleaseTag } = await stub.importFetchMocked('../src/vim.js'); 27 | detectLatestWindowsReleaseTagMocked = detectLatestWindowsReleaseTag; 28 | }); 29 | 30 | it('throws an error when response is other than 302', async function () { 31 | await A.rejects( 32 | () => detectLatestWindowsReleaseTagMocked(), 33 | /Expected status 302 \(Redirect\) but got 404/, 34 | ); 35 | }); 36 | }); 37 | }); 38 | 39 | describe('installVimOnWindows()', function () { 40 | it('throws an error when the specified version does not exist', async function () { 41 | for (const arch of ['x86_64', 'arm64'] as Arch[]) { 42 | await A.rejects( 43 | () => installVimOnWindows('v0.1.2', 'v0.1.2', arch), 44 | /^Error: Could not download and unarchive asset/, 45 | ); 46 | } 47 | }); 48 | 49 | context('with mocking fetch()', function () { 50 | let installVimOnWindowsMocked: typeof installVimOnWindows; 51 | const fetchStub = new FetchStub(); 52 | 53 | before(async function () { 54 | const { installVimOnWindows } = await fetchStub.importFetchMocked('../src/vim.js'); 55 | installVimOnWindowsMocked = installVimOnWindows; 56 | }); 57 | 58 | beforeEach(function () { 59 | fetchStub.reset(); 60 | }); 61 | 62 | it('throws an error when receiving unsuccessful response', async function () { 63 | await A.rejects( 64 | () => installVimOnWindowsMocked('v9.0.0', 'nightly', 'x86_64'), 65 | /Downloading asset failed: Not found for dummy/, 66 | ); 67 | }); 68 | 69 | it('falls back to x86_64 on error on arm64', async function () { 70 | await A.rejects( 71 | () => installVimOnWindowsMocked('v9.0.0', 'nightly', 'arm64'), 72 | /Downloading asset failed: Not found for dummy/, 73 | ); 74 | const expected = [ 75 | 'https://github.com/vim/vim-win32-installer/releases/download/v9.0.0/gvim_9.0.0_arm64.zip', 76 | 'https://github.com/vim/vim-win32-installer/releases/download/v9.0.0/gvim_9.0.0_x64.zip', 77 | ]; 78 | A.deepStrictEqual(fetchStub.fetchedUrls, expected, `${fetchStub.fetchedUrls}`); 79 | }); 80 | }); 81 | }); 82 | 83 | describe('buildVim()', function () { 84 | const stub = new ExecStub(); 85 | let buildVimMocked: typeof buildVim; 86 | const savedXcode11Env = process.env['XCODE_11_DEVELOPER_DIR']; 87 | 88 | before(async function () { 89 | const { buildVim } = await stub.importWithMock('../src/vim.js'); 90 | buildVimMocked = buildVim; 91 | process.env['XCODE_11_DEVELOPER_DIR'] = './'; 92 | }); 93 | 94 | after(function () { 95 | process.env['XCODE_11_DEVELOPER_DIR'] = savedXcode11Env; 96 | }); 97 | 98 | afterEach(function () { 99 | stub.reset(); 100 | }); 101 | 102 | it('builds nightly Vim from source', async function () { 103 | const installed = await buildVimMocked('nightly', 'linux', null); 104 | A.equal(installed.executable, 'vim'); 105 | A.ok(installed.binDir.endsWith(path.join('vim-nightly', 'bin')), installed.binDir); 106 | A.ok(installed.vimDir.endsWith(path.join('vim-nightly', 'share', 'vim')), installed.vimDir); 107 | A.ok(stub.called.length > 0); 108 | 109 | const [cmd, args] = stub.called[0]; 110 | A.equal(cmd, 'git'); 111 | A.equal(args[0], 'clone'); 112 | A.equal(args[args.length - 2], 'https://github.com/vim/vim'); 113 | // Nightly uses HEAD. It means tags are unnecessary 114 | A.equal(args[args.length - 3], '--no-tags'); 115 | 116 | A.equal(stub.called[1][0], './configure'); 117 | const configurePrefix = stub.called[1][1][0]; // --prefix=installDir 118 | A.equal(`--prefix=${path.dirname(installed.binDir)}`, configurePrefix); 119 | }); 120 | 121 | it('builds recent Vim from source', async function () { 122 | const version = 'v8.2.2424'; 123 | const installed = await buildVimMocked(version, 'linux', null); 124 | A.equal(installed.executable, 'vim'); 125 | A.ok(installed.binDir.endsWith(path.join(`vim-${version}`, 'bin')), installed.binDir); 126 | A.ok(installed.vimDir.endsWith(path.join(`vim-${version}`, 'share', 'vim')), installed.vimDir); 127 | A.ok(stub.called.length > 0); 128 | 129 | const [cmd, args] = stub.called[0]; 130 | A.equal(cmd, 'git'); 131 | A.equal(args[0], 'clone'); 132 | A.equal(args[args.length - 2], 'https://github.com/vim/vim'); 133 | // Specify tag name for cloning specific version 134 | A.equal(args[args.length - 4], '--branch'); 135 | A.equal(args[args.length - 3], version); 136 | 137 | A.equal(stub.called[1][0], './configure'); 138 | const configurePrefix = stub.called[1][1][0]; // --prefix=installDir 139 | A.equal(`--prefix=${path.dirname(installed.binDir)}`, configurePrefix); 140 | }); 141 | 142 | it('builds older Vim from source on macOS', async function () { 143 | const version = 'v8.2.0000'; 144 | await buildVimMocked(version, 'macos', null); 145 | 146 | // For older Vim (before 8.2.1119), Xcode11 is necessary to build 147 | // Check `./configure`, `make` and `make install` are run with Xcode11 148 | for (let i = 1; i < 4; i++) { 149 | const opts = stub.called[i][2]; 150 | A.ok(opts); 151 | A.ok('env' in opts); 152 | const env = opts['env']; 153 | A.ok('DEVELOPER_DIR' in env); 154 | } 155 | }); 156 | 157 | it('builds Vim from source with specified configure arguments', async function () { 158 | const version = 'v8.2.2424'; 159 | const installed = await buildVimMocked( 160 | version, 161 | 'linux', 162 | '--with-features=huge --enable-fail-if-missing --disable-nls', 163 | ); 164 | 165 | const [cmd, args] = stub.called[1]; 166 | A.equal(cmd, './configure'); 167 | const expected = [ 168 | `--prefix=${path.dirname(installed.binDir)}`, 169 | '--with-features=huge', 170 | '--enable-fail-if-missing', 171 | '--disable-nls', 172 | ]; 173 | A.deepEqual(args, expected); 174 | }); 175 | }); 176 | 177 | describe('versionIsOlderThan()', function () { 178 | const testCases: [string, boolean][] = [ 179 | // Equal 180 | ['v8.2.1119', false], 181 | // Newer 182 | ['v8.2.1120', false], 183 | ['v8.3.0000', false], 184 | ['v8.3.1234', false], 185 | ['v8.3.0123', false], 186 | ['v9.0.0000', false], 187 | // Older 188 | ['v8.2.1118', true], 189 | ['v8.2.0000', true], 190 | ['v8.1.2000', true], 191 | ['v8.1.1000', true], 192 | ['v8.0.2000', true], 193 | ['v7.3.2000', true], 194 | ['v7.2', true], 195 | ['v6.4', true], 196 | // Invalid 197 | ['8.2.1119', false], // 'v' prefix not found 198 | ['8.2', false], // newer than v7 but patch version does not exist 199 | ]; 200 | 201 | for (const tc of testCases) { 202 | const [v, expected] = tc; 203 | 204 | it(`${v} is ${expected ? 'older than' : 'equal or newer than'} 8.2.1119`, function () { 205 | A.equal(versionIsOlderThan(v, 8, 2, 1119), expected); 206 | }); 207 | } 208 | }); 209 | 210 | describe('getRuntimeDirInVimDir', function () { 211 | it('detects vim{ver} directory', async function () { 212 | const vimdir = path.join(TESTDATA_PATH, 'vimdir', 'vim_ver'); 213 | const runtime = await getRuntimeDirInVimDir(vimdir); 214 | A.ok(runtime, path.join(vimdir, 'vim91')); 215 | }); 216 | 217 | it('detects runtime directory', async function () { 218 | const vimdir = path.join(TESTDATA_PATH, 'vimdir', 'vim_runtime'); 219 | const runtime = await getRuntimeDirInVimDir(vimdir); 220 | A.ok(runtime, path.join(vimdir, 'runtime')); 221 | }); 222 | 223 | it('throws an error when the vimdir is not found', async function () { 224 | const vimdir = path.join(TESTDATA_PATH, 'vimdir', 'this-directory-does-not-exist'); 225 | await A.rejects(() => getRuntimeDirInVimDir(vimdir), /Could not read \$VIMDIR directory/); 226 | }); 227 | 228 | it('throws an error when no runtime directory is found', async function () { 229 | const vimdir = path.join(TESTDATA_PATH, 'vimdir', 'empty'); 230 | await A.rejects( 231 | () => getRuntimeDirInVimDir(vimdir), 232 | /Vim directory such as 'vim82' or 'runtime' was not found/, 233 | ); 234 | }); 235 | }); 236 | -------------------------------------------------------------------------------- /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 { TmpDir, type Os, type Arch, ensureError, getSystemHttpsProxyAgent } 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 tmpDir = await TmpDir.create(); 64 | const dir = path.join(tmpDir.path, 'vim'); 65 | 66 | try { 67 | { 68 | const args = ['clone', '--depth=1', '--single-branch']; 69 | if (version === 'nightly') { 70 | args.push('--no-tags'); 71 | } else { 72 | args.push('--branch', version); 73 | } 74 | args.push('https://github.com/vim/vim', dir); 75 | 76 | await exec('git', args); 77 | } 78 | 79 | const env: Env = {}; 80 | if (os === 'macos' && versionIsOlderThan(version, 8, 2, 1119)) { 81 | const dir = await getXcode11DevDir(); 82 | if (dir !== null) { 83 | // Vim before v8.2.1119 cannot be built with Xcode 12 or later. It requires Xcode 11. 84 | // ref: https://github.com/vim/vim/commit/5289783e0b07cfc3f92ee933261ca4c4acdca007 85 | // By setting $DEVELOPER_DIR environment variable, Xcode11 is used to build Vim. 86 | // ref: https://www.jessesquires.com/blog/2020/01/06/selecting-an-xcode-version-on-github-ci/ 87 | // Note that xcode-select command is not available since it changes Xcode version in system global. 88 | env['DEVELOPER_DIR'] = dir; 89 | core.debug( 90 | `Building Vim older than 8.2.1119 on macOS with Xcode11 at ${dir} instead of the latest Xcode`, 91 | ); 92 | } else { 93 | core.warning( 94 | `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`, 95 | ); 96 | } 97 | } 98 | 99 | const opts = { cwd: dir, env }; 100 | { 101 | const args = [`--prefix=${installDir}`]; 102 | if (configureArgs === null) { 103 | args.push('--with-features=huge', '--enable-fail-if-missing'); 104 | } else { 105 | args.push(...shlexSplit(configureArgs)); 106 | } 107 | try { 108 | await exec('./configure', args, opts); 109 | } catch (err) { 110 | if (os === 'macos' && versionIsOlderThan(version, 8, 2, 5135)) { 111 | core.warning( 112 | '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', 113 | ); 114 | } 115 | throw err; 116 | } 117 | } 118 | await exec('make', ['-j'], opts); 119 | await exec('make', ['install'], opts); 120 | core.debug(`Built and installed Vim to ${installDir} (version=${version})`); 121 | } finally { 122 | await tmpDir.cleanup(); 123 | } 124 | 125 | return { 126 | executable: exeName(os), 127 | binDir: path.join(installDir, 'bin'), 128 | vimDir: path.join(installDir, 'share', 'vim'), 129 | }; 130 | } 131 | 132 | // See `:help $VIMRUNTIME` for the detail of rules of Vim runtime directory 133 | export async function getRuntimeDirInVimDir(dir: string): Promise { 134 | // Search root Vim directory such as 'vim82' in unarchived directory 135 | let entries; 136 | try { 137 | entries = await fs.readdir(dir); 138 | } catch (e) { 139 | const err = ensureError(e); 140 | throw new Error(`Could not read $VIMDIR directory to detect vim executable: ${err}`); 141 | } 142 | const re = /^vim\d+$/; 143 | for (const entry of entries) { 144 | if (re.test(entry)) { 145 | const p = path.join(dir, entry); 146 | const s = await fs.stat(p); 147 | if (s.isDirectory()) { 148 | return p; 149 | } 150 | } else if (entry === 'runtime') { 151 | return path.join(dir, entry); 152 | } 153 | } 154 | throw new Error( 155 | `Vim directory such as 'vim82' or 'runtime' was not found in ${JSON.stringify(entries)} in unarchived directory '${dir}'`, 156 | ); 157 | } 158 | 159 | export async function detectLatestWindowsReleaseTag(): Promise { 160 | const url = 'https://github.com/vim/vim-win32-installer/releases/latest'; 161 | try { 162 | const res = await fetch(url, { 163 | method: 'HEAD', 164 | redirect: 'manual', 165 | agent: getSystemHttpsProxyAgent(url), 166 | }); 167 | 168 | if (res.status !== 302) { 169 | throw new Error(`Expected status 302 (Redirect) but got ${res.status} (${res.statusText})`); 170 | } 171 | 172 | const location = res.headers.get('location'); 173 | if (!location) { 174 | throw new Error(`'Location' header is not included in a response: ${JSON.stringify(res.headers.raw())}`); 175 | } 176 | 177 | const m = location.match(/\/releases\/tag\/(.+)$/); 178 | if (m === null) { 179 | throw new Error(`Unexpected redirect to ${location}. Redirected URL is not for release`); 180 | } 181 | 182 | core.debug(`Latest Vim release tag ${m[1]} was extracted from redirect`); 183 | return m[1]; 184 | } catch (e) { 185 | const err = ensureError(e); 186 | core.debug(err.stack ?? err.message); 187 | throw new Error(`${err.message}: Could not get latest release tag from ${url}`); 188 | } 189 | } 190 | 191 | async function installVimAssetOnWindows(file: string, url: string, dirSuffix: string): Promise { 192 | const tmpDir = await TmpDir.create(); 193 | const dlDir = path.join(tmpDir.path, 'vim-installer'); 194 | await io.mkdirP(dlDir); 195 | const assetFile = path.join(dlDir, file); 196 | const destDir = path.join(homedir(), `vim-${dirSuffix}`); 197 | 198 | try { 199 | core.debug(`Downloading asset at ${url} to ${dlDir}`); 200 | const response = await fetch(url, { agent: getSystemHttpsProxyAgent(url) }); 201 | if (!response.ok) { 202 | throw new Error(`Downloading asset failed: ${response.statusText}`); 203 | } 204 | const buffer = await response.arrayBuffer(); 205 | await fs.writeFile(assetFile, Buffer.from(buffer), { encoding: null }); 206 | core.debug(`Downloaded installer from ${url} to ${assetFile}`); 207 | 208 | await unzip(assetFile, dlDir); 209 | 210 | const vimDir = path.join(dlDir, 'vim'); // Unarchived to 'vim' directory 211 | core.debug(`Unzipped installer from ${url} to ${vimDir}`); 212 | 213 | await io.mv(vimDir, destDir); 214 | core.debug(`Vim was installed to ${destDir}`); 215 | } catch (e) { 216 | const err = ensureError(e); 217 | core.debug(err.stack ?? err.message); 218 | throw new Error(`Could not download and unarchive asset ${url} at ${dlDir}: ${err.message}`); 219 | } finally { 220 | await tmpDir.cleanup(); 221 | } 222 | 223 | return destDir; 224 | } 225 | 226 | export async function installVimOnWindows(tag: string, version: string, arch: Arch): Promise { 227 | core.debug(`Installing ${version} Vim from tag ${tag} on ${arch} Windows`); 228 | 229 | const ver = tag.slice(1); // Strip 'v' prefix 230 | // e.g. https://github.com/vim/vim-win32-installer/releases/download/v8.2.0158/gvim_8.2.0158_x64.zip 231 | const a = arch === 'x86_64' ? 'x64' : 'arm64'; 232 | const url = `https://github.com/vim/vim-win32-installer/releases/download/${tag}/gvim_${ver}_${a}.zip`; 233 | const file = `gvim_${ver}_${a}.zip`; 234 | let vimDir; 235 | try { 236 | vimDir = await installVimAssetOnWindows(file, url, version); 237 | } catch (e) { 238 | if (arch !== 'arm64') { 239 | throw e; 240 | } 241 | const err = ensureError(e); 242 | core.warning(`Fall back to x86_64 build because downloading Vim for arm64 windows from ${url} failed: ${err}`); 243 | return installVimOnWindows(tag, version, 'x86_64'); 244 | } 245 | const executable = exeName('windows'); 246 | const runtimeDir = await getRuntimeDirInVimDir(vimDir); 247 | 248 | // From v9.1.0631, vim.exe and gvim.exe share the same core, so OLE is enabled even in vim.exe. 249 | // This command registers the vim64.dll as a type library. Without the command, vim.exe will 250 | // ask the registration with GUI dialog and the process looks hanging. (#37) 251 | // 252 | // See: https://github.com/vim/vim/issues/15372 253 | if (version === 'stable' || version === 'nightly' || !versionIsOlderThan(version, 9, 1, 631)) { 254 | const bin = path.join(runtimeDir, executable); 255 | await exec(bin, ['-silent', '-register']); 256 | core.debug('Registered vim.exe as a type library'); 257 | } 258 | 259 | // vim.exe and gvim.exe are put in the runtime directory (e.g. `vim/vim91/vim.exe`) 260 | return { executable, binDir: runtimeDir, vimDir }; 261 | } 262 | 263 | export async function installNightlyVimOnWindows(version: string, arch: Arch): Promise { 264 | const latestTag = await detectLatestWindowsReleaseTag(); 265 | return installVimOnWindows(latestTag, version, arch); 266 | } 267 | -------------------------------------------------------------------------------- /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 { ExecStub, FetchStub } 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 | it('respects https_proxy environment variable', async function () { 20 | const saved = process.env; 21 | try { 22 | process.env = { ...saved, https_proxy: 'https://localhost:5678' }; 23 | // This promise is rejected because localhost:5678 doesn't serve a https proxy 24 | await A.rejects(() => downloadNeovim('stable', 'linux', 'x86_64'), /Could not download Neovim release/); 25 | } finally { 26 | process.env = saved; 27 | } 28 | }); 29 | 30 | context('with mocking fetch()', function () { 31 | let downloadNeovimMocked: typeof downloadNeovim; 32 | let downloadStableNeovimMocked: typeof downloadStableNeovim; 33 | const fetchStub = new FetchStub(); 34 | 35 | before(async function () { 36 | const { downloadNeovim, downloadStableNeovim } = await fetchStub.importFetchMocked('../src/neovim.js'); 37 | downloadNeovimMocked = downloadNeovim; 38 | downloadStableNeovimMocked = downloadStableNeovim; 39 | }); 40 | 41 | beforeEach(function () { 42 | fetchStub.reset(); 43 | }); 44 | 45 | it('throws an error when receiving unsuccessful response', async function () { 46 | try { 47 | const ret = await downloadNeovimMocked('nightly', 'linux', 'x86_64'); 48 | A.ok(false, `Exception was not thrown: ${JSON.stringify(ret)}`); 49 | } catch (err) { 50 | const msg = (err as Error).message; 51 | A.ok(msg.includes('Could not download Neovim release from'), msg); 52 | A.ok(msg.includes('check the asset for linux was really uploaded'), msg); 53 | // Special message only for nightly build 54 | A.ok(msg.includes('Note that some assets are sometimes missing on nightly build'), msg); 55 | } 56 | }); 57 | 58 | it('fallbacks to the latest version detected from GitHub API', async function () { 59 | const token = process.env['GITHUB_TOKEN'] ?? null; 60 | if (token === null) { 61 | this.skip(); // GitHub API token is necessary 62 | } 63 | try { 64 | const ret = await downloadStableNeovimMocked('linux', 'x86_64', token); 65 | A.ok(false, `Exception was not thrown: ${JSON.stringify(ret)}`); 66 | } catch (e) { 67 | const err = e as Error; 68 | // Matches to version tag like '/v0.4.4/' as part of download URL in error message 69 | // Note: assert.match is not available in Node v12 70 | A.ok(/\/v\d+\.\d+\.\d+\//.test(err.message), err.message); 71 | } 72 | }); 73 | 74 | it('falls back to x86_64 on error on arm64 Windows', async function () { 75 | await A.rejects( 76 | downloadNeovimMocked('nightly', 'windows', 'arm64'), 77 | /Could not download Neovim release from/, 78 | ); 79 | const expected = [ 80 | 'https://github.com/neovim/neovim/releases/download/nightly/nvim-win-arm64.zip', 81 | 'https://github.com/neovim/neovim/releases/download/nightly/nvim-win64.zip', 82 | ]; 83 | A.deepStrictEqual(fetchStub.fetchedUrls, expected, `${fetchStub.fetchedUrls}`); 84 | }); 85 | }); 86 | }); 87 | 88 | describe('buildNightlyNeovim()', function () { 89 | const stub = new ExecStub(); 90 | let buildNightlyNeovimMocked: typeof buildNightlyNeovim; 91 | 92 | before(async function () { 93 | const { buildNightlyNeovim } = await stub.importWithMock('../src/neovim.js'); 94 | buildNightlyNeovimMocked = buildNightlyNeovim; 95 | }); 96 | 97 | afterEach(function () { 98 | stub.reset(); 99 | }); 100 | 101 | it('builds nightly Neovim on Linux', async function () { 102 | const installed = await buildNightlyNeovimMocked('linux'); 103 | A.equal(installed.executable, 'nvim'); 104 | A.ok(installed.binDir.endsWith(path.join('nvim-nightly', 'bin')), installed.binDir); 105 | const installDir = path.dirname(installed.binDir); 106 | 107 | // apt-get -> git -> make 108 | const apt = stub.called[0]; 109 | A.ok(apt[0] === 'sudo' && apt[1][0] === 'apt-get', JSON.stringify(apt)); 110 | const make = stub.called[2]; 111 | A.equal(make[0], 'make'); 112 | const makeArgs = make[1]; 113 | A.ok(makeArgs[1].endsWith(installDir), `${makeArgs}`); 114 | }); 115 | 116 | it('builds nightly Neovim on macOS', async function () { 117 | const installed = await buildNightlyNeovimMocked('macos'); 118 | A.equal(installed.executable, 'nvim'); 119 | A.ok(installed.binDir.endsWith(path.join('nvim-nightly', 'bin')), installed.binDir); 120 | const installDir = path.dirname(installed.binDir); 121 | 122 | // brew -> git -> make 123 | const brew = stub.called[0]; 124 | A.ok(brew[0] === 'brew', JSON.stringify(brew)); 125 | const make = stub.called[2]; 126 | A.equal(make[0], 'make'); 127 | const makeArgs = make[1]; 128 | A.ok(makeArgs[1].endsWith(installDir), `${makeArgs}`); 129 | }); 130 | 131 | it('throws an error on Windows', async function () { 132 | await A.rejects( 133 | () => buildNightlyNeovimMocked('windows'), 134 | /Building Neovim from source is not supported for windows/, 135 | ); 136 | }); 137 | }); 138 | 139 | describe('assetDirName', function () { 140 | it('returns "Neovim" when Neovim version is earlier than 0.7 on Windows', function () { 141 | A.equal(assetDirName('v0.6.1', 'windows', 'x86_64'), 'Neovim'); 142 | A.equal(assetDirName('v0.4.3', 'windows', 'x86_64'), 'Neovim'); 143 | A.equal(assetDirName('v0.6.1', 'windows', 'arm64'), 'Neovim'); 144 | }); 145 | 146 | it('returns "nvim-win64" when Neovim version is 0.7 or later on Windows', function () { 147 | A.equal(assetDirName('v0.7.0', 'windows', 'x86_64'), 'nvim-win64'); 148 | A.equal(assetDirName('v0.10.0', 'windows', 'x86_64'), 'nvim-win64'); 149 | A.equal(assetDirName('v1.0.0', 'windows', 'x86_64'), 'nvim-win64'); 150 | A.equal(assetDirName('nightly', 'windows', 'x86_64'), 'nvim-win64'); 151 | A.equal(assetDirName('stable', 'windows', 'x86_64'), 'nvim-win64'); 152 | }); 153 | 154 | it('returns "nvim-win-arm64" at Neovim after v0.11.4 on Windows', function () { 155 | A.equal(assetDirName('v0.11.4', 'windows', 'arm64'), 'nvim-win64'); 156 | A.equal(assetDirName('nightly', 'windows', 'arm64'), 'nvim-win-arm64'); 157 | }); 158 | 159 | it('returns "nvim-osx64" when Neovim version is earlier than 0.7.1 on macOS', function () { 160 | A.equal(assetDirName('v0.7.0', 'macos', 'x86_64'), 'nvim-osx64'); 161 | A.equal(assetDirName('v0.6.1', 'macos', 'x86_64'), 'nvim-osx64'); 162 | }); 163 | 164 | it('returns "nvim-macos" when Neovim version is 0.7.1 or later and 0.9.5 or earlier on macOS', function () { 165 | A.equal(assetDirName('v0.7.1', 'macos', 'x86_64'), 'nvim-macos'); 166 | A.equal(assetDirName('v0.8.0', 'macos', 'x86_64'), 'nvim-macos'); 167 | A.equal(assetDirName('v0.9.5', 'macos', 'x86_64'), 'nvim-macos'); 168 | }); 169 | 170 | 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 () { 171 | A.equal(assetDirName('v0.10.0', 'macos', 'x86_64'), 'nvim-macos-x86_64'); 172 | A.equal(assetDirName('v1.0.0', 'macos', 'x86_64'), 'nvim-macos-x86_64'); 173 | A.equal(assetDirName('stable', 'macos', 'x86_64'), 'nvim-macos-x86_64'); 174 | A.equal(assetDirName('nightly', 'macos', 'x86_64'), 'nvim-macos-x86_64'); 175 | A.equal(assetDirName('v0.10.0', 'macos', 'arm64'), 'nvim-macos-arm64'); 176 | A.equal(assetDirName('v1.0.0', 'macos', 'arm64'), 'nvim-macos-arm64'); 177 | A.equal(assetDirName('stable', 'macos', 'arm64'), 'nvim-macos-arm64'); 178 | A.equal(assetDirName('nightly', 'macos', 'arm64'), 'nvim-macos-arm64'); 179 | }); 180 | 181 | it('returns "nvim-linux64" when Neovim version is earlier than 0.10.4 on Linux', function () { 182 | A.equal(assetDirName('v0.10.3', 'linux', 'x86_64'), 'nvim-linux64'); 183 | A.equal(assetDirName('v0.9.5', 'linux', 'x86_64'), 'nvim-linux64'); 184 | A.throws( 185 | () => assetDirName('v0.10.3', 'linux', 'arm64'), 186 | /^Error: Linux arm64 has been only supported since Neovim v0\.10\.4/, 187 | ); 188 | A.throws( 189 | () => assetDirName('v0.9.5', 'linux', 'arm64'), 190 | /^Error: Linux arm64 has been only supported since Neovim v0\.10\.4/, 191 | ); 192 | }); 193 | 194 | it('returns "nvim-linux-x86_64" or "nvim-linux-arm64" when Neovim version is earlier than 0.10.4 on Linux', function () { 195 | A.equal(assetDirName('v0.10.4', 'linux', 'x86_64'), 'nvim-linux-x86_64'); 196 | A.equal(assetDirName('v0.11.0', 'linux', 'x86_64'), 'nvim-linux-x86_64'); 197 | A.equal(assetDirName('v0.10.4', 'linux', 'arm64'), 'nvim-linux-arm64'); 198 | A.equal(assetDirName('v0.11.0', 'linux', 'arm64'), 'nvim-linux-arm64'); 199 | A.equal(assetDirName('stable', 'linux', 'x86_64'), 'nvim-linux-x86_64'); 200 | A.equal(assetDirName('stable', 'linux', 'arm64'), 'nvim-linux-arm64'); 201 | }); 202 | 203 | it('throws an error on arm32 Linux', function () { 204 | A.throws(() => assetDirName('v0.10.3', 'linux', 'arm32'), /^Error: Unsupported CPU architecture/); 205 | A.throws(() => assetDirName('stable', 'linux', 'arm32'), /^Error: Unsupported CPU architecture/); 206 | }); 207 | }); 208 | 209 | describe('assetFileName', function () { 210 | it('returns asset file name following the Neovim version and CPU arch on Linux', function () { 211 | A.equal(assetFileName('v0.10.3', 'linux', 'x86_64'), 'nvim-linux64.tar.gz'); 212 | A.equal(assetFileName('v0.10.4', 'linux', 'x86_64'), 'nvim-linux-x86_64.tar.gz'); 213 | A.equal(assetFileName('v0.10.4', 'linux', 'arm64'), 'nvim-linux-arm64.tar.gz'); 214 | }); 215 | 216 | it('returns asset file name following the Neovim version and CPU arch on macOS', function () { 217 | A.equal(assetFileName('v0.7.0', 'macos', 'x86_64'), 'nvim-macos.tar.gz'); 218 | A.equal(assetFileName('v0.7.1', 'macos', 'x86_64'), 'nvim-macos.tar.gz'); 219 | A.equal(assetFileName('v0.10.4', 'macos', 'x86_64'), 'nvim-macos-x86_64.tar.gz'); 220 | A.equal(assetFileName('v0.10.4', 'macos', 'arm64'), 'nvim-macos-arm64.tar.gz'); 221 | }); 222 | 223 | it('returns asset file name following the Neovim version and CPU arch on Windows', function () { 224 | A.equal(assetFileName('v0.11.4', 'windows', 'x86_64'), 'nvim-win64.zip'); 225 | A.equal(assetFileName('nightly', 'windows', 'x86_64'), 'nvim-win64.zip'); 226 | A.equal(assetFileName('v0.11.4', 'windows', 'arm64'), 'nvim-win64.zip'); // arm64 build is not released yet 227 | A.equal(assetFileName('nightly', 'windows', 'arm64'), 'nvim-win-arm64.zip'); 228 | }); 229 | }); 230 | }); 231 | -------------------------------------------------------------------------------- /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 { TmpDir, type Os, type Arch, ensureError, getSystemHttpsProxyAgent } 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 | switch (arch) { 55 | case 'x86_64': 56 | return 'nvim-win64.zip'; 57 | case 'arm64': 58 | // At point of v0.11.4, arm64 build is not available. It may be released at the next version. 59 | if (version === 'nightly') { 60 | return 'nvim-win-arm64.zip'; 61 | } else { 62 | return 'nvim-win64.zip'; 63 | } 64 | default: 65 | throw Error(`Unsupported CPU architecture for Neovim ${version} on ${os}: ${arch}`); // Should be unreachable 66 | } 67 | } 68 | } 69 | 70 | export function assetDirName(version: string, os: Os, arch: Arch): string { 71 | switch (os) { 72 | case 'macos': { 73 | const v = parseVersion(version); 74 | if (v !== null) { 75 | // Until v0.7.0 release, 'nvim-osx64' was the asset directory name on macOS. However it was changed to 76 | // 'nvim-macos' from v0.7.1: https://github.com/neovim/neovim/pull/19029 77 | if (v.minor < 7 || (v.minor === 7 && v.patch < 1)) { 78 | return 'nvim-osx64'; 79 | } 80 | // Until v0.9.5, the single asset nvim-macos.tar.gz is released. From v0.10.0, Neovim provides 81 | // nvim-macos-arm64.tar.gz (for Apple Silicon) and nvim-macos-x86_64.tar.gz (for Intel Mac). (#30) 82 | if (v.minor < 10) { 83 | return 'nvim-macos'; 84 | } 85 | } 86 | switch (arch) { 87 | case 'arm64': 88 | return 'nvim-macos-arm64'; 89 | case 'x86_64': 90 | return 'nvim-macos-x86_64'; 91 | default: 92 | throw Error(`Unsupported CPU architecture for Neovim ${version} on ${os}: ${arch}`); // Should be unreachable 93 | } 94 | } 95 | case 'linux': { 96 | const v = parseVersion(version); 97 | if (v !== null && (v.minor < 10 || (v.minor === 10 && v.patch < 4))) { 98 | switch (arch) { 99 | case 'arm64': 100 | throw Error( 101 | `Linux arm64 has been only supported since Neovim v0.10.4 but the requested version is ${version}`, 102 | ); 103 | case 'x86_64': 104 | return 'nvim-linux64'; 105 | default: 106 | break; 107 | } 108 | } 109 | switch (arch) { 110 | case 'arm64': 111 | return 'nvim-linux-arm64'; 112 | case 'x86_64': 113 | return 'nvim-linux-x86_64'; 114 | default: 115 | throw Error(`Unsupported CPU architecture for Neovim ${version} on ${os}: ${arch}`); // Should be unreachable 116 | } 117 | } 118 | case 'windows': { 119 | // Until v0.6.1 release, 'Neovim' was the asset directory name on Windows. However it was changed to 'nvim-win64' 120 | // from v0.7.0. (#20) 121 | const v = parseVersion(version); 122 | if (v !== null && v.minor < 7) { 123 | return 'Neovim'; 124 | } 125 | switch (arch) { 126 | case 'x86_64': 127 | return 'nvim-win64'; 128 | case 'arm64': 129 | // At point of v0.11.4, arm64 build is not available. It may be released at the next version. 130 | if (version === 'nightly') { 131 | return 'nvim-win-arm64'; 132 | } else { 133 | return 'nvim-win64'; 134 | } 135 | default: 136 | throw Error(`Unsupported CPU architecture for Neovim ${version} on ${os}: ${arch}`); // Should be unreachable 137 | } 138 | } 139 | } 140 | } 141 | 142 | async function unarchiveAsset(asset: string, dirName: string): Promise { 143 | const dir = path.dirname(asset); 144 | const dest = path.join(dir, dirName); 145 | if (asset.endsWith('.tar.gz')) { 146 | await exec('tar', ['xzf', asset], { cwd: dir }); 147 | return dest; 148 | } 149 | if (asset.endsWith('.zip')) { 150 | await unzip(asset, dir); 151 | return dest; 152 | } 153 | throw new Error(`FATAL: Don't know how to unarchive ${asset} to ${dest}`); 154 | } 155 | 156 | // version = 'stable' or 'nightly' or version string 157 | export async function downloadNeovim(version: string, os: Os, arch: Arch): Promise { 158 | const file = assetFileName(version, os, arch); 159 | const destDir = path.join(homedir(), `nvim-${version}`); 160 | const url = `https://github.com/neovim/neovim/releases/download/${version}/${file}`; 161 | core.info(`Downloading Neovim ${version} on ${os} from ${url} to ${destDir}`); 162 | 163 | const tmpDir = await TmpDir.create(); 164 | const asset = path.join(tmpDir.path, file); 165 | 166 | try { 167 | core.debug(`Downloading asset ${asset}`); 168 | const response = await fetch(url, { agent: getSystemHttpsProxyAgent(url) }); 169 | if (!response.ok) { 170 | throw new Error(`Downloading asset failed: ${response.statusText}`); 171 | } 172 | const buffer = await response.arrayBuffer(); 173 | await fs.writeFile(asset, Buffer.from(buffer), { encoding: null }); 174 | core.debug(`Downloaded asset ${asset}`); 175 | 176 | const unarchived = await unarchiveAsset(asset, assetDirName(version, os, arch)); 177 | core.debug(`Unarchived asset ${unarchived}`); 178 | 179 | await io.mv(unarchived, destDir); 180 | core.debug(`Installed Neovim ${version} on ${os} to ${destDir}`); 181 | 182 | return { 183 | executable: exeName(os), 184 | binDir: path.join(destDir, 'bin'), 185 | vimDir: path.join(destDir, 'share', 'nvim'), 186 | }; 187 | } catch (e) { 188 | const err = ensureError(e); 189 | core.debug(err.stack ?? err.message); 190 | 191 | if (os === 'windows' && arch === 'arm64') { 192 | core.warning( 193 | `Fall back to x86_64 build because downloading Neovim for arm64 windows from ${url} failed: ${err}`, 194 | ); 195 | return await downloadNeovim(version, os, 'x86_64'); 196 | } 197 | 198 | 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`; 199 | if (version === 'nightly') { 200 | msg += ". Note that some assets are sometimes missing on nightly build due to Neovim's CI failure"; 201 | } 202 | throw new Error(msg); 203 | } finally { 204 | await tmpDir.cleanup(); 205 | } 206 | } 207 | 208 | async function fetchLatestVersion(token: string): Promise { 209 | const octokit = github.getOctokit(token, { 210 | request: { agent: getSystemHttpsProxyAgent('https://api.github.com') }, 211 | }); 212 | const { data } = await octokit.rest.repos.listReleases({ owner: 'neovim', repo: 'neovim' }); 213 | const re = /^v\d+\.\d+\.\d+$/; 214 | for (const release of data) { 215 | const tagName = release.tag_name; 216 | if (re.test(tagName)) { 217 | core.debug(`Detected the latest stable version '${tagName}'`); 218 | return tagName; 219 | } 220 | } 221 | core.debug(`No stable version was found in releases: ${JSON.stringify(data, null, 2)}`); 222 | throw new Error(`No stable version was found in ${data.length} releases`); 223 | } 224 | 225 | // Download stable asset from 'stable' release. When the asset is not found, get the latest version 226 | // using GitHub API and retry downloading an asset with the version as fallback (#5). 227 | export async function downloadStableNeovim(os: Os, arch: Arch, token: string | null = null): Promise { 228 | try { 229 | return await downloadNeovim('stable', os, arch); // `await` is necessary to catch excetipn 230 | } catch (e) { 231 | const err = ensureError(e); 232 | if (err.message.includes('Downloading asset failed:') && token !== null) { 233 | core.warning( 234 | `Could not download stable asset. Detecting the latest stable release from GitHub API as fallback: ${err.message}`, 235 | ); 236 | const ver = await fetchLatestVersion(token); 237 | core.warning(`Fallback to install asset from '${ver}' release`); 238 | return downloadNeovim(ver, os, arch); 239 | } 240 | throw err; 241 | } 242 | } 243 | 244 | // Build nightly Neovim from sources as fallback of downloading nightly assets from the nightly release page of 245 | // neovim/neovim repository (#18). 246 | // https://github.com/neovim/neovim/wiki/Building-Neovim 247 | export async function buildNightlyNeovim(os: Os): Promise { 248 | core.debug(`Installing Neovim by building from source on ${os}`); 249 | 250 | switch (os) { 251 | case 'linux': 252 | core.debug('Installing build dependencies via apt'); 253 | await exec('sudo', [ 254 | 'apt-get', 255 | 'install', 256 | '-y', 257 | '--no-install-recommends', 258 | 'ninja-build', 259 | 'gettext', 260 | 'libtool', 261 | 'libtool-bin', 262 | 'autoconf', 263 | 'automake', 264 | 'cmake', 265 | 'g++', 266 | 'pkg-config', 267 | 'unzip', 268 | 'curl', 269 | ]); 270 | break; 271 | case 'macos': 272 | core.debug('Installing build dependencies via Homebrew'); 273 | await exec('brew', [ 274 | 'install', 275 | 'ninja', 276 | 'libtool', 277 | 'automake', 278 | 'cmake', 279 | 'pkg-config', 280 | 'gettext', 281 | 'curl', 282 | '--quiet', 283 | ]); 284 | break; 285 | default: 286 | throw new Error(`Building Neovim from source is not supported for ${os} platform`); 287 | } 288 | 289 | // Add -nightly suffix since building stable Neovim from source may be supported in the future 290 | const installDir = path.join(homedir(), 'nvim-nightly'); 291 | core.debug(`Building and installing Neovim to ${installDir}`); 292 | const tmpDir = await TmpDir.create(); 293 | try { 294 | const dir = path.join(tmpDir.path, 'build-nightly-neovim'); 295 | 296 | await exec('git', ['clone', '--depth=1', 'https://github.com/neovim/neovim.git', dir]); 297 | 298 | const opts = { cwd: dir }; 299 | const makeArgs = [ 300 | '-j', 301 | `CMAKE_EXTRA_FLAGS=-DCMAKE_INSTALL_PREFIX=${installDir}`, 302 | 'CMAKE_BUILD_TYPE=RelWithDebug', 303 | ]; 304 | await exec('make', makeArgs, opts); 305 | core.debug(`Built Neovim in ${opts.cwd}. Installing it via 'make install'`); 306 | await exec('make', ['install'], opts); 307 | core.debug(`Installed Neovim to ${installDir}`); 308 | } finally { 309 | await tmpDir.cleanup(); 310 | } 311 | 312 | return { 313 | executable: exeName(os), 314 | binDir: path.join(installDir, 'bin'), 315 | vimDir: path.join(installDir, 'share', 'nvim'), 316 | }; 317 | } 318 | -------------------------------------------------------------------------------- /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 the following outputs which can be used by later steps. 105 | 106 | ### `executable` output 107 | 108 | Absolute path to the installed executable. You can use it for running Vim command in the following 109 | steps. 110 | 111 | Here is an example to set Vim executable to run unit tests with [themis.vim][vim-themis]. 112 | 113 | ```yaml 114 | - uses: actions/checkout@v5 115 | with: 116 | repository: thinca/vim-themis 117 | path: vim-themis 118 | - uses: rhysd/action-setup-vim@v1 119 | id: vim 120 | - name: Run unit tests with themis.vim 121 | env: 122 | THEMIS_VIM: ${{ steps.vim.outputs.executable }} 123 | run: | 124 | ./vim-themis/bin/themis ./test 125 | ``` 126 | 127 | ### `vim-dir` output 128 | 129 | Absolute path to the installed `$VIM` directory. For more details about the directory, please see 130 | `:help $VIM` in your Vim or Neovim. 131 | 132 | This output is useful when you want to refer the `$VIM` directory in the following steps. 133 | 134 | Here is an example to put a configuration specific to Vim installed by this action. 135 | 136 | ```yaml 137 | - uses: rhysd/action-setup-vim@v1 138 | id: vim 139 | - name: Setup vimrc specific to the Vim 140 | run: | 141 | cp vimrc_for_ci.vim '${{ vim.vim-dir }}/vimrc' 142 | ``` 143 | 144 | And here is another example to remove the default configurations which can affect test execution. 145 | 146 | ```yaml 147 | - uses: rhysd/action-setup-vim@v1 148 | id: vim 149 | - name: Remove the default configurations 150 | run: | 151 | rm -f ${{ vim.vim-dir }}/{vim*,runtime}/defaults.vim 152 | ``` 153 | 154 | ## Supported platforms 155 | 156 | | | Vim | Neovim | 157 | |---------------------------|-----------------------------|---------------------------------------| 158 | | Linux x86_64 | :white_check_mark: | :white_check_mark: | 159 | | Linux arm64 | :white_check_mark: | :warning: since v0.10.4 | 160 | | Linux arm32 (self-hosted) | :white_check_mark: | :x: | 161 | | Windows x86_64 | :warning: no stable version | :white_check_mark: | 162 | | Windows arm64 | :warning: no stable version | :warning: x86 emulation until v0.11.4 | 163 | | macOS x86_64 | :white_check_mark: | :white_check_mark: | 164 | | macOS arm64 | :white_check_mark: | :white_check_mark: | 165 | 166 | - :white_check_mark: : Supported 167 | - :warning: : Supported with limitation 168 | - :x: : Unsupported 169 | 170 | ## Installation details 171 | 172 | ### Vim 173 | 174 | `vX.Y.Z` represents a specific version such as `v8.2.0126`. 175 | 176 | | OS | Version | Installation | 177 | |---------|-----------|-----------------------------------------------------------------------------| 178 | | Linux | `stable` | Install [`vim-gtk3`][vim-gtk3] via `apt` package manager | 179 | | Linux | `nightly` | Build the HEAD of [vim/vim][vim] repository | 180 | | Linux | `vX.Y.Z` | Build the `vX.Y.Z` tag of [vim/vim][vim] repository | 181 | | macOS | `stable` | Install MacVim via `brew install macvim` | 182 | | macOS | `nightly` | Build the HEAD of [vim/vim][vim] repository | 183 | | macOS | `vX.Y.Z` | Build the `vX.Y.Z` tag of [vim/vim][vim] repository | 184 | | Windows | `stable` | There is no stable release for Windows so fall back to `nightly` | 185 | | Windows | `nightly` | Install the latest release from [the installer repository][win-inst] | 186 | | Windows | `vX.Y.Z` | Install the release at `vX.Y.Z` tag of [the installer repository][win-inst] | 187 | 188 | For stable releases on all platforms and nightly on Windows, `gvim` executable is also available. 189 | 190 | When installing without system's package manager, Vim is installed at `$HOME/vim-{version}` (e.g. 191 | `~/vim-stable`). 192 | 193 | **Note:** When you build Vim older than 8.2.1119 on macOS, Xcode 11 or earlier is necessary due to 194 | lack of [this patch][vim_8_2_1119]. Please try `macos-11` runner instead of the latest macOS runner 195 | in the case. 196 | 197 | **Note:** When a Windows arm64 binary is not included in the release due to some reason (e.g. build 198 | error), it falls back to x86_64 binary. It will be executed via x86 emulation on Arm Windows. 199 | 200 | ### Neovim 201 | 202 | `vX.Y.Z` represents a specific version such as `v0.4.3`. 203 | 204 | | OS | Version | Installation | 205 | |---------|-----------|---------------------------------------------------------------------------| 206 | | Linux | `stable` | Install from the latest [Neovim stable release][nvim-stable] | 207 | | Linux | `nightly` | Install from the latest [Neovim nightly release][nvim-nightly] | 208 | | Linux | `vX.Y.Z` | Install the release at `vX.Y.Z` tag of [neovim/neovim][neovim] repository | 209 | | macOS | `stable` | `brew install neovim` using Homebrew | 210 | | macOS | `nightly` | Install from the latest [Neovim nightly release][nvim-nightly] | 211 | | macOS | `vX.Y.Z` | Install the release at `vX.Y.Z` tag of [neovim/neovim][neovim] repository | 212 | | Windows | `stable` | Install from the latest [Neovim stable release][nvim-stable] | 213 | | Windows | `nightly` | Install from the latest [Neovim nightly release][nvim-nightly] | 214 | | Windows | `vX.Y.Z` | Install the release at `vX.Y.Z` tag of [neovim/neovim][neovim] repository | 215 | 216 | Only on Windows, `nvim-qt.exe` executable is available for GUI. 217 | 218 | When installing without system's package manager, Neovim is installed at `$HOME/nvim-{version}` 219 | (e.g. `~/nvim-stable`). 220 | 221 | **Note:** Ubuntu 18.04 supports official [`neovim` package][ubuntu-nvim] but this action does not 222 | install it. As of now, GitHub Actions also supports Ubuntu 16.04. 223 | 224 | **Note:** When downloading a Neovim asset from [`stable` release][nvim-stable] on GitHub, the asset 225 | is rarely missing in the release. In the case, this action will get the latest version tag from 226 | GitHub API and use it instead of `stable` tag (see [#5][issue-5] for more details). 227 | 228 | **Note:** When downloading a Neovim asset from [`nightly` release][nvim-nightly] on GitHub, it might 229 | cause 'Asset Not Found' error. This is because the Nightly build failed due to some reason in 230 | [neovim/neovim][neovim] CI workflow. In the case, this action tries to build Neovim from sources on 231 | Linux and macOS workers. It gives up installation on other platforms. 232 | 233 | **Note:** Linux arm64 binaries for `ubuntu-24.04-arm` runner are supported since v0.11.4. 234 | 235 | **Note:** Windows arm64 binaries are currently support only on `nightly`. When an arm64 binary is 236 | not included in the release due to some reason (e.g. build error), it falls back to x86_64 binary. 237 | It will be executed via x86 emulation on Arm Windows. 238 | 239 | ## Choosing a specific version 240 | 241 | ### Vim 242 | 243 | If Vim is built from source, any tag version should be available. 244 | 245 | If Vim is installed via release asset (on Windows), please check 246 | [vim-win32-installer releases page][win-inst-release] to know which versions are available. 247 | The repository makes a release once per day (nightly). 248 | 249 | Note that Vim's patch number in version tags is in 4-digits like `v8.2.0126`. Omitting leading 250 | zeros such as `v8.2.126` or `v8.2.1` is not allowed. 251 | 252 | ### Neovim 253 | 254 | When installing the specific version of Neovim, this action downloads release assets from 255 | [neovim/neovim][neovim]. Please check [neovim/neovim releases page][neovim-release] to know which 256 | versions have release assets. For example, 257 | [Neovim 0.4.0](https://github.com/neovim/neovim/releases/tag/v0.4.0) has no Windows releases so it 258 | is not available for installing Neovim on Windows. 259 | 260 | ## Current limitation 261 | 262 | - GUI version (gVim and nvim-qt) is supported partially as described in above section. 263 | - Installing Vim/Neovim from system's package manager is not configurable. For example, arguments 264 | cannot be passed to `brew install`. 265 | 266 | These are basically not a technical limitation. Please let me know by creating an issue if you want 267 | some of them. 268 | 269 | ## License 270 | 271 | Distributed under [the MIT license](./LICENSE.txt). 272 | 273 | [ci-badge]: https://github.com/rhysd/action-setup-vim/actions/workflows/ci.yml/badge.svg 274 | [ci]: https://github.com/rhysd/action-setup-vim/actions/workflows/ci.yml 275 | [release-badge]: https://img.shields.io/github/v/release/rhysd/action-setup-vim.svg 276 | [marketplace]: https://github.com/marketplace/actions/setup-vim 277 | [proj]: https://github.com/rhysd/action-setup-vim 278 | [github-actions]: https://github.com/features/actions 279 | [vim]: https://github.com/vim/vim 280 | [neovim]: https://github.com/neovim/neovim 281 | [win-inst]: https://github.com/vim/vim-win32-installer 282 | [nvim-stable]: https://github.com/neovim/neovim/releases/tag/stable 283 | [nvim-nightly]: https://github.com/neovim/neovim/releases/tag/nightly 284 | [clever-f-workflow]: https://github.com/rhysd/clever-f.vim/blob/master/.github/workflows/ci.yml 285 | [git-messenger-workflow]: https://github.com/rhysd/git-messenger.vim/blob/master/.github/workflows/ci.yml 286 | [vim-gtk3]: https://packages.ubuntu.com/search?keywords=vim-gtk3 287 | [ubuntu-nvim]: https://packages.ubuntu.com/search?keywords=neovim 288 | [vim-themis]: https://github.com/thinca/vim-themis 289 | [win-inst-release]: https://github.com/vim/vim-win32-installer/releases 290 | [neovim-release]: https://github.com/neovim/neovim/releases 291 | [generate-pat]: https://github.com/settings/tokens/new 292 | [gh-action-secrets]: https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets 293 | [issue-5]: https://github.com/rhysd/action-setup-vim/issues/5 294 | [vim_8_2_1119]: https://github.com/vim/vim/commit/5289783e0b07cfc3f92ee933261ca4c4acdca007 295 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # [v1.6.0](https://github.com/rhysd/action-setup-vim/releases/tag/v1.6.0) - 2025-11-09 3 | 4 | - Add new `vim-dir` output which stores the `$VIM` directory absolute path of the installation. This output is useful to locate where the Vim/Neovim is installed by the following steps. Here is an example of setting the installation-specific `vimrc` configuration: 5 | ```yaml 6 | - uses: rhysd/action-setup-vim@v1 7 | id: vim 8 | - name: Setup vimrc specific to the Vim installation 9 | run: | 10 | cp vimrc_for_ci.vim '${{ vim.vim-dir }}/vimrc' 11 | ``` 12 | - Fix the Vim installation directory structure on Windows. It did not conform the standard directory layout which Vim assumes. Previously the runtime directory was wrongly squashed. The paths before/after this release when installing `stable` version are: 13 | - Previous wrong path: `~/vim-stable/vim.exe` 14 | - New correct path: `~/vim-stable/vim91/vim.exe` 15 | - Clean up temporary directories created by this action after installation completes. Temporary directories containing downloaded assets were not removed previously but they could cause troubles when installing multiple Vim/Neovim instances. 16 | - Report logs via [`core.info()`](https://github.com/actions/toolkit/tree/main/packages/core#logging) instead of directly calling `console.log()`. 17 | 18 | [Changes][v1.6.0] 19 | 20 | 21 | 22 | # [v1.5.1](https://github.com/rhysd/action-setup-vim/releases/tag/v1.5.1) - 2025-11-02 23 | 24 | - Fix installing stable Vim on `macos-15-intel` runner. It was broken again after v1.4.5 release due to the change in the upstream runner. ([#52](https://github.com/rhysd/action-setup-vim/issues/52)) 25 | - Update `@actions/io` to v2.0.0 for better Node.js v24 support. 26 | 27 | [Changes][v1.5.1] 28 | 29 | 30 | 31 | # [v1.5.0](https://github.com/rhysd/action-setup-vim/releases/tag/v1.5.0) - 2025-10-26 32 | 33 | - Support arm64 native binaries on [`windows-11-arm` runner](https://github.blog/changelog/2025-04-14-windows-arm64-hosted-runners-now-available-in-public-preview/). 34 | - When an arm64 binary is missing in the release due to some reasons (e.g. build error), it falls back to x86_64 binary so that your workflow can continue. In this case the x86_64 binary is executed via x86 emulation. 35 | - Neovim only supports arm64 binaries on nightly builds for now. 36 | - Migrate the action runtime from `node20` to `node24`. This change only affects users running this action on self-hosted runners. 37 | - Fix the validation for installed executables was not sufficient on Windows. 38 | 39 | [Changes][v1.5.0] 40 | 41 | 42 | 43 | # [v1.4.5](https://github.com/rhysd/action-setup-vim/releases/tag/v1.4.5) - 2025-10-12 44 | 45 | - Better workaround for [#52](https://github.com/rhysd/action-setup-vim/issues/52) to properly install stable Vim on macos-15-intel runner. We use `python@3.13` package which is already installed on the system instead of installing `python` package to remove conflicted symbolic links. It's faster and has less side effects. 46 | 47 | [Changes][v1.4.5] 48 | 49 | 50 | 51 | # [v1.4.4](https://github.com/rhysd/action-setup-vim/releases/tag/v1.4.4) - 2025-10-12 52 | 53 | - Fix stable Vim installation fails on `macos-15-intel` runner due to the version conflict in `python` Homebrew package. ([#52](https://github.com/rhysd/action-setup-vim/issues/52)) 54 | - Fix HTTPS proxy is not used when accessing GitHub API via Octokit client even if `https_proxy` environment variable is set. 55 | - Fix using a deprecated API in `node-fetch` package. 56 | 57 | [Changes][v1.4.4] 58 | 59 | 60 | 61 | # [v1.4.3](https://github.com/rhysd/action-setup-vim/releases/tag/v1.4.3) - 2025-09-07 62 | 63 | - Use HTTPS proxy looking at environment variables such as `https_proxy`, `no_proxy`, `all_proxy` when fetching assets. ([#50](https://github.com/rhysd/action-setup-vim/issues/50), thanks [@xieyonn](https://github.com/xieyonn)) 64 | - Implementation migrated from CommonJS to ES Modules. This should not affect the behavior of this action. 65 | - Reduce amount of log output when installing stable Vim on Linux. 66 | - Update dependencies including `@actions/github`. 67 | 68 | [Changes][v1.4.3] 69 | 70 | 71 | 72 | # [v1.4.2](https://github.com/rhysd/action-setup-vim/releases/tag/v1.4.2) - 2025-03-28 73 | 74 | - 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)) 75 | - Add a warning message with useful information when executing `./configure` fails to build older versions of Vim. 76 | - Update dependencies including some security fixes in `@octokit/*` packages. 77 | 78 | 79 | [Changes][v1.4.2] 80 | 81 | 82 | 83 | # [v1.4.1](https://github.com/rhysd/action-setup-vim/releases/tag/v1.4.1) - 2025-02-01 84 | 85 | - Fix arm32 Linux (self-hosted runner) is rejected on checking the CPU architecture before installation. 86 | - 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. 87 | 88 | [Changes][v1.4.1] 89 | 90 | 91 | 92 | # [v1.4.0](https://github.com/rhysd/action-setup-vim/releases/tag/v1.4.0) - 2025-02-01 93 | 94 | - 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)) 95 | - 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. 96 | - 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) 97 | 98 | [Changes][v1.4.0] 99 | 100 | 101 | 102 | # [v1.3.5](https://github.com/rhysd/action-setup-vim/releases/tag/v1.3.5) - 2024-07-28 103 | 104 | - Fix `vim` command hangs on Windows after Vim 9.1.0631. ([#37](https://github.com/rhysd/action-setup-vim/issues/37)) 105 | - 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). 106 | - Update the dependencies to the latest. This includes small security fixes. 107 | 108 | [Changes][v1.3.5] 109 | 110 | 111 | 112 | # [v1.3.4](https://github.com/rhysd/action-setup-vim/releases/tag/v1.3.4) - 2024-05-17 113 | 114 | - 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)) 115 | - 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. 116 | 117 | [Changes][v1.3.4] 118 | 119 | 120 | 121 | # [v1.3.3](https://github.com/rhysd/action-setup-vim/releases/tag/v1.3.3) - 2024-05-07 122 | 123 | - Remove the support for Ubuntu 18.04, which was removed from GitHub-hosted runners more than one year ago. 124 | - 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)) 125 | - Update dependencies including some security patches. 126 | 127 | [Changes][v1.3.3] 128 | 129 | 130 | 131 | # [v1.3.2](https://github.com/rhysd/action-setup-vim/releases/tag/v1.3.2) - 2024-03-29 132 | 133 | - 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)) 134 | - 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. 135 | - Update npm dependencies to the latest. This update includes some small security fixes. 136 | - Fix an incorrect OS version was reported in debug message on Ubuntu. 137 | 138 | [Changes][v1.3.2] 139 | 140 | 141 | 142 | # [v1.3.1](https://github.com/rhysd/action-setup-vim/releases/tag/v1.3.1) - 2024-01-31 143 | 144 | - 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)) 145 | - On M1 Mac, Homebrew installation directory was changed from `/usr/local` to `/opt/homebrew` 146 | 147 | [Changes][v1.3.1] 148 | 149 | 150 | 151 | # [v1.3.0](https://github.com/rhysd/action-setup-vim/releases/tag/v1.3.0) - 2023-10-15 152 | 153 | - `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)) 154 | ```yaml 155 | - uses: rhysd/action-setup-vim@v1 156 | with: 157 | version: 8.0.0000 158 | configure-args: | 159 | --with-features=huge --enable-fail-if-missing --disable-nls 160 | ``` 161 | - Update the action runtime to `node20`. Now this action is run with Node.js v20. 162 | - Update all dependencies to the latest including `@actions/github` v6.0.0 and some security fixes. 163 | 164 | [Changes][v1.3.0] 165 | 166 | 167 | 168 | # [v1.2.15](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.15) - 2023-03-06 169 | 170 | - 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)) 171 | - Upgrade the lock file version from v2 to v3, which largely reduces size of `package-lock.json`. 172 | - Update dependencies. 173 | 174 | [Changes][v1.2.15] 175 | 176 | 177 | 178 | # [v1.2.14](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.14) - 2023-01-09 179 | 180 | - 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. 181 | - 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. 182 | - Update dependencies to fix deprecation warning from `uuid` package 183 | 184 | [Changes][v1.2.14] 185 | 186 | 187 | 188 | # [v1.2.13](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.13) - 2022-10-13 189 | 190 | - 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. 191 | - Update other dependencies including `@actions/github` v5.1.1 192 | 193 | [Changes][v1.2.13] 194 | 195 | 196 | 197 | # [v1.2.12](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.12) - 2022-07-21 198 | 199 | - 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)) 200 | - Update dependencies including `@actions/core` v1.9.0 and `@actions/github` v5.0.3. 201 | 202 | [Changes][v1.2.12] 203 | 204 | 205 | 206 | # [v1.2.11](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.11) - 2022-04-15 207 | 208 | - 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. 209 | 210 | [Changes][v1.2.11] 211 | 212 | 213 | 214 | # [v1.2.10](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.10) - 2022-03-23 215 | 216 | - 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)) 217 | - Update dependencies to the latest. (including new `@actions/exec` and `@actions/io`) 218 | 219 | [Changes][v1.2.10] 220 | 221 | 222 | 223 | # [v1.2.9](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.9) - 2022-02-05 224 | 225 | - Use `node16` runner to run this action. 226 | - Update dependencies. Now TypeScript source compiles to ES2021 code since Node.js v16 supports all ES2021 features. 227 | 228 | [Changes][v1.2.9] 229 | 230 | 231 | 232 | # [v1.2.8](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.8) - 2021-10-02 233 | 234 | - 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)) 235 | - This fallback logic is currently only for Linux and macOS 236 | - 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) 237 | - Update many dependencies including all `@actions/*` packages and TypeScript compiler 238 | - 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. 239 | 240 | [Changes][v1.2.8] 241 | 242 | 243 | 244 | # [v1.2.7](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.7) - 2021-02-05 245 | 246 | - 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)) 247 | - Improve: Better error message on an invalid value for `version` input 248 | - Improve: Update dependencies 249 | 250 | [Changes][v1.2.7] 251 | 252 | 253 | 254 | # [v1.2.6](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.6) - 2020-11-15 255 | 256 | - 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)) 257 | - Improve: Update dependencies 258 | 259 | [Changes][v1.2.6] 260 | 261 | 262 | 263 | # [v1.2.5](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.5) - 2020-10-02 264 | 265 | - Fix: Update `@actions/core` for security patch 266 | - Improve: Internal refactoring 267 | - Improve: Update dependencies 268 | 269 | [Changes][v1.2.5] 270 | 271 | 272 | 273 | # [v1.2.4](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.4) - 2020-09-08 274 | 275 | - 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)) 276 | - Improve: Update dependencies to the latest 277 | 278 | [Changes][v1.2.4] 279 | 280 | 281 | 282 | # [v1.2.3](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.3) - 2020-03-29 283 | 284 | - Fix: Run `apt update` before `apt install` on installing stable Vim on Linux. `apt install vim-gnome` caused an error without this 285 | 286 | [Changes][v1.2.3] 287 | 288 | 289 | 290 | # [v1.2.2](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.2) - 2020-02-22 291 | 292 | - Improve: Better error message when no asset is found on installing Neovim 293 | 294 | [Changes][v1.2.2] 295 | 296 | 297 | 298 | # [v1.2.1](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.1) - 2020-02-15 299 | 300 | - Improve: Validate the executable file before getting `--version` output 301 | 302 | [Changes][v1.2.1] 303 | 304 | 305 | 306 | # [v1.2.0](https://github.com/rhysd/action-setup-vim/releases/tag/v1.2.0) - 2020-02-02 307 | 308 | - 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. 309 | - 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. 310 | 311 | [Changes][v1.2.0] 312 | 313 | 314 | 315 | # [v1.1.3](https://github.com/rhysd/action-setup-vim/releases/tag/v1.1.3) - 2020-01-31 316 | 317 | - 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) 318 | - Fix: Path separator was not correct on Windows 319 | - Improve: Better post-action validation on CI and internal refactoring 320 | 321 | [Changes][v1.1.3] 322 | 323 | 324 | 325 | # [v1.1.2](https://github.com/rhysd/action-setup-vim/releases/tag/v1.1.2) - 2020-01-31 326 | 327 | - 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)) 328 | 329 | [Changes][v1.1.2] 330 | 331 | 332 | 333 | # [v1.1.1](https://github.com/rhysd/action-setup-vim/releases/tag/v1.1.1) - 2020-01-31 334 | 335 | - Improve: `github-token` input is now optional even if you install Vim on Windows worker 336 | - Improve: Update dev-dependencies 337 | 338 | [Changes][v1.1.1] 339 | 340 | 341 | 342 | # [v1.1.0](https://github.com/rhysd/action-setup-vim/releases/tag/v1.1.0) - 2020-01-29 343 | 344 | - 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. 345 | 346 | [Changes][v1.1.0] 347 | 348 | 349 | 350 | # [v1.0.2](https://github.com/rhysd/action-setup-vim/releases/tag/v1.0.2) - 2020-01-28 351 | 352 | - Improve: Now all input environment variables (starting with `INPUT_`) are filtered on executing subprocesses ([actions/toolkit#309](https://github.com/actions/toolkit/issues/309)) 353 | - Improve: Unit tests were added for validation of inputs and outputs 354 | - Improve: Better validation error messages 355 | - Improve: Better descriptions in README.md 356 | 357 | [Changes][v1.0.2] 358 | 359 | 360 | 361 | # [v1.0.1](https://github.com/rhysd/action-setup-vim/releases/tag/v1.0.1) - 2020-01-25 362 | 363 | - Improve: Install stable Neovim with Homebrew on macOS. Now it is installed via `brew install neovim` 364 | 365 | [Changes][v1.0.1] 366 | 367 | 368 | 369 | # [v1.0.0](https://github.com/rhysd/action-setup-vim/releases/tag/v1.0.0) - 2020-01-24 370 | 371 | First release :tada: 372 | 373 | Please read [README.md](https://github.com/rhysd/action-setup-vim#readme) for usage. 374 | 375 | [Changes][v1.0.0] 376 | 377 | 378 | [v1.6.0]: https://github.com/rhysd/action-setup-vim/compare/v1.5.1...v1.6.0 379 | [v1.5.1]: https://github.com/rhysd/action-setup-vim/compare/v1.5.0...v1.5.1 380 | [v1.5.0]: https://github.com/rhysd/action-setup-vim/compare/v1.4.5...v1.5.0 381 | [v1.4.5]: https://github.com/rhysd/action-setup-vim/compare/v1.4.4...v1.4.5 382 | [v1.4.4]: https://github.com/rhysd/action-setup-vim/compare/v1.4.3...v1.4.4 383 | [v1.4.3]: https://github.com/rhysd/action-setup-vim/compare/v1.4.2...v1.4.3 384 | [v1.4.2]: https://github.com/rhysd/action-setup-vim/compare/v1.4.1...v1.4.2 385 | [v1.4.1]: https://github.com/rhysd/action-setup-vim/compare/v1.4.0...v1.4.1 386 | [v1.4.0]: https://github.com/rhysd/action-setup-vim/compare/v1.3.5...v1.4.0 387 | [v1.3.5]: https://github.com/rhysd/action-setup-vim/compare/v1.3.4...v1.3.5 388 | [v1.3.4]: https://github.com/rhysd/action-setup-vim/compare/v1.3.3...v1.3.4 389 | [v1.3.3]: https://github.com/rhysd/action-setup-vim/compare/v1.3.2...v1.3.3 390 | [v1.3.2]: https://github.com/rhysd/action-setup-vim/compare/v1.3.1...v1.3.2 391 | [v1.3.1]: https://github.com/rhysd/action-setup-vim/compare/v1.3.0...v1.3.1 392 | [v1.3.0]: https://github.com/rhysd/action-setup-vim/compare/v1.2.15...v1.3.0 393 | [v1.2.15]: https://github.com/rhysd/action-setup-vim/compare/v1.2.14...v1.2.15 394 | [v1.2.14]: https://github.com/rhysd/action-setup-vim/compare/v1.2.13...v1.2.14 395 | [v1.2.13]: https://github.com/rhysd/action-setup-vim/compare/v1.2.12...v1.2.13 396 | [v1.2.12]: https://github.com/rhysd/action-setup-vim/compare/v1.2.11...v1.2.12 397 | [v1.2.11]: https://github.com/rhysd/action-setup-vim/compare/v1.2.10...v1.2.11 398 | [v1.2.10]: https://github.com/rhysd/action-setup-vim/compare/v1.2.9...v1.2.10 399 | [v1.2.9]: https://github.com/rhysd/action-setup-vim/compare/v1.2.8...v1.2.9 400 | [v1.2.8]: https://github.com/rhysd/action-setup-vim/compare/v1.2.7...v1.2.8 401 | [v1.2.7]: https://github.com/rhysd/action-setup-vim/compare/v1.2.6...v1.2.7 402 | [v1.2.6]: https://github.com/rhysd/action-setup-vim/compare/v1.2.5...v1.2.6 403 | [v1.2.5]: https://github.com/rhysd/action-setup-vim/compare/v1.2.4...v1.2.5 404 | [v1.2.4]: https://github.com/rhysd/action-setup-vim/compare/v1.2.3...v1.2.4 405 | [v1.2.3]: https://github.com/rhysd/action-setup-vim/compare/v1.2.2...v1.2.3 406 | [v1.2.2]: https://github.com/rhysd/action-setup-vim/compare/v1.2.1...v1.2.2 407 | [v1.2.1]: https://github.com/rhysd/action-setup-vim/compare/v1.2.0...v1.2.1 408 | [v1.2.0]: https://github.com/rhysd/action-setup-vim/compare/v1.1.3...v1.2.0 409 | [v1.1.3]: https://github.com/rhysd/action-setup-vim/compare/v1.1.2...v1.1.3 410 | [v1.1.2]: https://github.com/rhysd/action-setup-vim/compare/v1.1.1...v1.1.2 411 | [v1.1.1]: https://github.com/rhysd/action-setup-vim/compare/v1.1.0...v1.1.1 412 | [v1.1.0]: https://github.com/rhysd/action-setup-vim/compare/v1.0.2...v1.1.0 413 | [v1.0.2]: https://github.com/rhysd/action-setup-vim/compare/v1.0.1...v1.0.2 414 | [v1.0.1]: https://github.com/rhysd/action-setup-vim/compare/v1.0.0...v1.0.1 415 | [v1.0.0]: https://github.com/rhysd/action-setup-vim/tree/v1.0.0 416 | 417 | 418 | --------------------------------------------------------------------------------