├── .github └── workflows │ └── build.yml ├── LICENSE.txt ├── README.md ├── action.yml └── index.js /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ${{ matrix.os }} 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | os: [ubuntu-24.04, ubuntu-24.04-arm, ubuntu-22.04, ubuntu-22.04-arm, macos-15, macos-14, macos-13, windows-2025, windows-2022, windows-2019] 10 | elasticsearch-version: [9, 8] 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: ./. 14 | with: 15 | elasticsearch-version: ${{ matrix.elasticsearch-version }} 16 | plugins: | 17 | analysis-kuromoji 18 | analysis-smartcn 19 | config: | 20 | cluster.name: my-cluster 21 | http.port: 9201 22 | - run: curl -s localhost:9201 23 | - run: curl -s localhost:9201 | grep my-cluster 24 | - run: which elasticsearch 25 | - run: elasticsearch-plugin list 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020-2025 Andrew Kane 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 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # setup-elasticsearch 2 | 3 | The missing action for Elasticsearch :tada: 4 | 5 | - Simpler than containers 6 | - Works on Linux, Mac, and Windows 7 | - Supports different versions 8 | 9 | [![Build Status](https://github.com/ankane/setup-elasticsearch/actions/workflows/build.yml/badge.svg)](https://github.com/ankane/setup-elasticsearch/actions) 10 | 11 | ## Getting Started 12 | 13 | Add it as a step to your workflow 14 | 15 | ```yml 16 | - uses: ankane/setup-elasticsearch@v1 17 | ``` 18 | 19 | ## Versions 20 | 21 | Specify a version (defaults to the latest) 22 | 23 | ```yml 24 | - uses: ankane/setup-elasticsearch@v1 25 | with: 26 | elasticsearch-version: 9 27 | ``` 28 | 29 | Supports major versions (`9`, `8`), minor versions (`9.0`, `8.18`, etc), and full versions (`9.0.0`, `8.18.0`, etc) 30 | 31 | Test against multiple versions 32 | 33 | ```yml 34 | strategy: 35 | matrix: 36 | elasticsearch-version: [9, 8] 37 | steps: 38 | - uses: ankane/setup-elasticsearch@v1 39 | with: 40 | elasticsearch-version: ${{ matrix.elasticsearch-version }} 41 | ``` 42 | 43 | ## Options 44 | 45 | Install plugins 46 | 47 | ```yml 48 | - uses: ankane/setup-elasticsearch@v1 49 | with: 50 | plugins: | 51 | analysis-kuromoji 52 | analysis-smartcn 53 | ``` 54 | 55 | Set `elasticsearch.yml` config 56 | 57 | ```yml 58 | - uses: ankane/setup-elasticsearch@v1 59 | with: 60 | config: | 61 | http.port: 9200 62 | ``` 63 | 64 | ## Caching [experimental] 65 | 66 | Add a step to your workflow **before** the `setup-elasticsearch` one 67 | 68 | ```yml 69 | - uses: actions/cache@v4 70 | with: 71 | path: ~/elasticsearch 72 | key: ${{ runner.os }}-elasticsearch-${{ matrix.elasticsearch-version }} 73 | ``` 74 | 75 | ## Related Actions 76 | 77 | - [setup-postgres](https://github.com/ankane/setup-postgres) 78 | - [setup-mysql](https://github.com/ankane/setup-mysql) 79 | - [setup-mariadb](https://github.com/ankane/setup-mariadb) 80 | - [setup-mongodb](https://github.com/ankane/setup-mongodb) 81 | - [setup-sqlserver](https://github.com/ankane/setup-sqlserver) 82 | 83 | ## Contributing 84 | 85 | Everyone is encouraged to help improve this project. Here are a few ways you can help: 86 | 87 | - [Report bugs](https://github.com/ankane/setup-elasticsearch/issues) 88 | - Fix bugs and [submit pull requests](https://github.com/ankane/setup-elasticsearch/pulls) 89 | - Write, clarify, or fix documentation 90 | - Suggest or add new features 91 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: Setup Elasticsearch 2 | inputs: 3 | elasticsearch-version: 4 | description: The Elasticsearch version to download (if necessary) and use 5 | plugins: 6 | description: Elasticsearch plugins to install 7 | config: 8 | description: Set config 9 | runs: 10 | using: node20 11 | main: index.js 12 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const spawnSync = require('child_process').spawnSync; 2 | const fs = require('fs'); 3 | const os = require('os'); 4 | const path = require('path'); 5 | const process = require('process'); 6 | 7 | const versionMap = { 8 | '9': '9.0.0', 9 | '8': '8.18.0', 10 | '7': '7.17.28', 11 | '9.0': '9.0.0', 12 | '8.18': '8.18.0', 13 | '8.17': '8.17.4', 14 | '8.16': '8.16.6', 15 | '8.15': '8.15.5', 16 | '8.14': '8.14.3', 17 | '8.13': '8.13.4', 18 | '8.12': '8.12.2', 19 | '8.11': '8.11.4', 20 | '8.10': '8.10.4', 21 | '8.9': '8.9.2', 22 | '8.8': '8.8.2', 23 | '8.7': '8.7.1', 24 | '8.6': '8.6.2', 25 | '8.5': '8.5.3', 26 | '8.4': '8.4.3', 27 | '8.3': '8.3.3', 28 | '8.2': '8.2.3', 29 | '8.1': '8.1.3', 30 | '8.0': '8.0.1', 31 | '7.17': '7.17.28', 32 | '7.16': '7.16.3', 33 | '7.15': '7.15.2', 34 | '7.14': '7.14.2', 35 | '7.13': '7.13.4', 36 | '7.12': '7.12.1', 37 | '7.11': '7.11.1', 38 | '7.10': '7.10.2', 39 | '7.9': '7.9.3', 40 | '7.8': '7.8.1', 41 | '7.7': '7.7.1', 42 | '7.6': '7.6.2', 43 | '7.5': '7.5.2', 44 | '7.4': '7.4.2', 45 | '7.3': '7.3.2', 46 | '7.2': '7.2.1', 47 | '7.1': '7.1.1', 48 | '7.0': '7.0.1' 49 | }; 50 | 51 | function run() { 52 | const args = Array.from(arguments); 53 | console.log(args.join(' ')); 54 | const command = args.shift(); 55 | // spawn is safer and more lightweight than exec 56 | const ret = spawnSync(command, args, {stdio: 'inherit'}); 57 | if (ret.status !== 0) { 58 | throw ret.error; 59 | } 60 | } 61 | 62 | // only use with validated input 63 | // https://github.com/nodejs/node/issues/52554 64 | function runBat() { 65 | const args = Array.from(arguments); 66 | console.log(args.join(' ')); 67 | const command = args.shift(); 68 | if (!fs.existsSync(command)) { 69 | throw 'Bat not found'; 70 | } 71 | const ret = spawnSync(command, args, {stdio: 'inherit', shell: true}); 72 | if (ret.status !== 0) { 73 | throw ret.error; 74 | } 75 | } 76 | 77 | function addToEnv(value) { 78 | fs.appendFileSync(process.env.GITHUB_ENV, `${value}\n`); 79 | } 80 | 81 | function addToPath(value) { 82 | fs.appendFileSync(process.env.GITHUB_PATH, `${value}\n`); 83 | } 84 | 85 | function getVersion() { 86 | let version = process.env['INPUT_ELASTICSEARCH-VERSION'] || '9'; 87 | if (versionMap[version]) { 88 | version = versionMap[version]; 89 | } 90 | if (!/^[789]\.\d{1,2}\.\d{1,2}$/.test(version)) { 91 | throw `Elasticsearch version not supported: ${version}`; 92 | } 93 | return version; 94 | } 95 | 96 | function isWindows() { 97 | return process.platform == 'win32'; 98 | } 99 | 100 | // no JDK version is ideal, but deprecated 101 | function getUrl() { 102 | let url; 103 | if (process.platform == 'darwin') { 104 | if (process.arch == 'arm64') { 105 | url = `https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-${elasticsearchVersion}-darwin-aarch64.tar.gz`; 106 | } else { 107 | url = `https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-${elasticsearchVersion}-darwin-x86_64.tar.gz`; 108 | } 109 | } else if (isWindows()) { 110 | url = `https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-${elasticsearchVersion}-windows-x86_64.zip`; 111 | } else { 112 | if (process.arch == 'arm64') { 113 | url = `https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-${elasticsearchVersion}-linux-aarch64.tar.gz`; 114 | } else { 115 | url = `https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-${elasticsearchVersion}-linux-x86_64.tar.gz`; 116 | } 117 | } 118 | return url; 119 | } 120 | 121 | function download() { 122 | const url = getUrl(); 123 | if (isWindows()) { 124 | run('curl', '-s', '-o', 'elasticsearch.zip', url); 125 | run('unzip', '-q', 'elasticsearch.zip'); 126 | } else { 127 | run('wget', '-q', '-O', 'elasticsearch.tar.gz', url); 128 | run('tar', 'xfz', 'elasticsearch.tar.gz'); 129 | } 130 | if (!fs.existsSync(cacheDir)) { 131 | fs.mkdirSync(cacheDir, {recursive: true}); 132 | } 133 | if (isWindows()) { 134 | // fix for: cross-device link not permitted 135 | run('mv', `elasticsearch-${elasticsearchVersion}`, esHome) 136 | } else { 137 | fs.renameSync(`elasticsearch-${elasticsearchVersion}`, esHome); 138 | } 139 | } 140 | 141 | // log4j 142 | // Elasticsearch 6 and 7 are not susceptible to RCE due to Java Security Manager 143 | // set flag to prevent information leak via DNS 144 | // https://discuss.elastic.co/t/apache-log4j2-remote-code-execution-rce-vulnerability-cve-2021-44228-esa-2021-31/291476 145 | function fixLog4j() { 146 | const jvmOptionsPath = path.join(esHome, 'config', 'jvm.options'); 147 | if (!fs.readFileSync(jvmOptionsPath).includes('log4j2.formatMsgNoLookups')) { 148 | fs.appendFileSync(jvmOptionsPath, '\n-Dlog4j2.formatMsgNoLookups=true\n'); 149 | 150 | // needed for Elasticsearch < 6.5 151 | // but remove for all versions 152 | if (!isWindows()) { 153 | const coreJarPath = fs.readdirSync(path.join(esHome, 'lib')).filter(fn => fn.includes('log4j-core-'))[0]; 154 | if (coreJarPath) { 155 | run('zip', '-q', '-d', path.join(esHome, 'lib', coreJarPath), 'org/apache/logging/log4j/core/lookup/JndiLookup.class'); 156 | } 157 | } else if (elasticsearchVersion < '6.5') { 158 | throw 'Elasticsearch version not available'; 159 | } 160 | } 161 | } 162 | 163 | function installPlugins() { 164 | let plugins = (process.env['INPUT_PLUGINS'] || '').trim(); 165 | if (plugins.length > 0) { 166 | console.log('Installing plugins'); 167 | 168 | // split here instead of above since JS returns [''] for empty array 169 | plugins = plugins.split(/\s*[,\n]\s*/); 170 | 171 | // validate 172 | // do not change without checking impact on runBat 173 | plugins.forEach( function(plugin) { 174 | if (!/^\w(\w|-)+$/.test(plugin)) { 175 | throw `Invalid plugin: ${plugin}`; 176 | } 177 | }); 178 | 179 | // install multiple plugins at once with Elasticsearch 7.6+ 180 | // https://www.elastic.co/guide/en/elasticsearch/plugins/7.6/installing-multiple-plugins.html 181 | const versionParts = elasticsearchVersion.split('.'); 182 | const atOnce = parseInt(versionParts[0]) >= 7 && parseInt(versionParts[1]) >= 6; 183 | let pluginCmd = path.join(esHome, 'bin', 'elasticsearch-plugin'); 184 | let runCmd = run; 185 | if (isWindows()) { 186 | pluginCmd += '.bat'; 187 | runCmd = runBat; 188 | } 189 | if (atOnce) { 190 | runCmd(pluginCmd, 'install', '--silent', '--batch', ...plugins); 191 | } else { 192 | plugins.forEach( function(plugin) { 193 | runCmd(pluginCmd, 'install', '--silent', '--batch', plugin); 194 | }); 195 | } 196 | } 197 | } 198 | 199 | function setConfig(dir) { 200 | let config = process.env['INPUT_CONFIG'] || ''; 201 | config += '\n'; 202 | config += 'discovery.type: single-node\n'; 203 | 204 | const [majorVersion, minorVersion, patchVersion] = elasticsearchVersion.split('.'); 205 | if (parseInt(majorVersion) >= 8 || parseInt(minorVersion) >= 13) { 206 | config += 'xpack.security.enabled: false\n'; 207 | } 208 | 209 | const file = path.join(dir, 'config', 'elasticsearch.yml'); 210 | // overwrite instead of append to play nicely with caching 211 | // alternatively, could append to copy of original file 212 | fs.writeFileSync(file, config); 213 | } 214 | 215 | function startServer() { 216 | if (isWindows()) { 217 | const serviceCmd = path.join(esHome, 'bin', 'elasticsearch-service.bat'); 218 | runBat(serviceCmd, 'install'); 219 | runBat(serviceCmd, 'start'); 220 | } else { 221 | run(path.join(esHome, 'bin', 'elasticsearch'), '-d'); 222 | } 223 | } 224 | 225 | function getPort() { 226 | const config = process.env['INPUT_CONFIG'] || ''; 227 | const match = config.match(/\bhttp\.port: +(\d{4,5})\b/); 228 | return match ? parseInt(match[1]) : 9200; 229 | } 230 | 231 | function waitForReady() { 232 | console.log("Waiting for server to be ready"); 233 | for (let i = 0; i < 30; i++) { 234 | let ret = spawnSync('curl', ['-s', `localhost:${getPort()}`]); 235 | if (ret.status === 0) { 236 | break; 237 | } 238 | spawnSync('sleep', ['1']); 239 | } 240 | } 241 | 242 | const elasticsearchVersion = getVersion(); 243 | const cacheDir = path.join(os.homedir(), 'elasticsearch'); 244 | const esHome = path.join(cacheDir, elasticsearchVersion); 245 | 246 | // java compatibility 247 | // https://www.elastic.co/support/matrix 248 | const majorVersion = parseInt(elasticsearchVersion.split('.')[0]); 249 | const javaHome = majorVersion == 7 ? process.env.JAVA_HOME_11_X64 : (majorVersion == 8 ? process.env.JAVA_HOME_17_X64 : process.env.JAVA_HOME_21_X64); 250 | if (javaHome) { 251 | if (majorVersion == 7) { 252 | process.env.JAVA_HOME = javaHome; 253 | addToEnv(`JAVA_HOME=${javaHome}`); 254 | } else { 255 | process.env.ES_JAVA_HOME = javaHome; 256 | addToEnv(`ES_JAVA_HOME=${javaHome}`); 257 | } 258 | } 259 | 260 | if (!fs.existsSync(esHome)) { 261 | const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'elasticsearch-')); 262 | process.chdir(tmpDir); 263 | download(); 264 | fixLog4j(); 265 | installPlugins(); 266 | } else { 267 | console.log('Elasticsearch cached'); 268 | fixLog4j(); 269 | } 270 | 271 | setConfig(esHome); 272 | startServer(); 273 | 274 | waitForReady(); 275 | 276 | addToEnv(`ES_HOME=${esHome}`); 277 | addToPath(path.join(esHome, 'bin')); 278 | --------------------------------------------------------------------------------