├── .gitignore ├── docs ├── assets │ ├── js │ │ ├── 11.28002d60.js │ │ ├── 2.d8f505d4.js │ │ ├── 9.dca480d7.js │ │ ├── 10.94ab74aa.js │ │ ├── 4.9602ac92.js │ │ ├── 7.e7d4a4a6.js │ │ ├── 8.c52cc8a3.js │ │ └── 3.dd60ef97.js │ └── img │ │ └── search.83621669.svg ├── logging.md ├── installation.md ├── authentication.md ├── .vuepress │ └── config.js ├── 404.html ├── environment.md ├── README.md ├── failure.md ├── components.md ├── installation.html ├── authentication.html ├── logging.html ├── commands.md ├── environment.html └── failure.html ├── test ├── fixtures │ ├── prepend.fab │ ├── prompt.fab │ ├── logs │ │ ├── server1-log.log │ │ ├── server2-log.log │ │ ├── ssh-log.log │ │ ├── local-log.log │ │ └── global-log.log │ ├── logging.fab │ ├── handler.fab │ ├── simple.fab │ ├── keys │ │ ├── ssh.public │ │ └── ssh.private │ ├── advanced.fab │ └── fabula.js ├── unit │ ├── __snapshots__ │ │ └── handling.test.js.snap │ ├── handling.test.js │ ├── logging.test.js │ └── compiler.test.js ├── util.js ├── bin.js └── server.js ├── deploy.sh ├── src ├── index.js ├── commands │ ├── index.js │ ├── get.js │ ├── put.js │ ├── exec.js │ ├── fabula.js │ ├── cd.js │ ├── ensure.js │ ├── write.js │ └── handle.js ├── utils.js ├── prompt.js ├── local.js ├── ssh.js ├── cli.js ├── run.js ├── logging.js ├── command.js └── compile.js ├── .babelrc ├── .eslintrc ├── fabula.js ├── bin └── fabula.js ├── recipes ├── setup-local-ssh.fab ├── setup-github-deployment.fab └── install-nginx-on-ubuntu.fab ├── .github └── main.workflow ├── dist ├── fabula-get.js ├── fabula-put.js ├── fabula-fabula.js ├── fabula-chunk3.js ├── fabula-cd.js ├── fabula-ensure.js ├── fabula-write.js ├── fabula-handle.js ├── fabula-chunk.js └── fabula.js ├── jest.config.js ├── README.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | node_modules 3 | .vuepress/dist 4 | -------------------------------------------------------------------------------- /docs/assets/js/11.28002d60.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[11],{167:function(n,w,o){}}]); -------------------------------------------------------------------------------- /test/fixtures/prepend.fab: -------------------------------------------------------------------------------- 1 | 2 | touch /usr/bin/sudotest 3 | service restart nginx 4 | write /tmp/file: 5 | test with prepend 6 | 7 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | rm -rf docs/assets 2 | vuepress build docs 3 | mv docs/.vuepress/dist/* docs/ 4 | rm -rf docs/.vuepress/dist/ 5 | git add docs 6 | git commit -m "docs: build update" 7 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | export { default as cli } from './cli' 3 | export { compile } from './compile' 4 | export { loadConfig } from './cli' 5 | export { parseArgv } from './command' 6 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"], 3 | "env": { 4 | "test": { 5 | "plugins": [ 6 | "babel-plugin-dynamic-import-node" 7 | ] 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /test/fixtures/prompt.fab: -------------------------------------------------------------------------------- 1 | 2 | export default async ({ prompt }) => ({ 3 | configOption: await prompt('Config option:') 4 | }) 5 | 6 | 7 | 8 | local echo <%= configOption %> 9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/logs/server1-log.log: -------------------------------------------------------------------------------- 1 | [server1] "server1 success test" 2 | [server1] [OK] bin.js --code 0 --stdout "server1 success test" 3 | [server1] "server1 error test" 4 | [server1] [FAIL] bin.js --code 1 --stderr "server1 error test" 5 | -------------------------------------------------------------------------------- /test/fixtures/logs/server2-log.log: -------------------------------------------------------------------------------- 1 | [server2] "server2 success test" 2 | [server2] [OK] bin.js --code 0 --stdout "server2 success test" 3 | [server2] "server2 error test" 4 | [server2] [FAIL] bin.js --code 1 --stderr "server2 error test" 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | root: true, 3 | parserOptions: { 4 | parser: 'babel-eslint', 5 | sourceType: 'module', 6 | ecmaFeatures: { 7 | legacyDecorators: true 8 | } 9 | }, 10 | extends: [ 11 | '@nuxtjs' 12 | ] 13 | } -------------------------------------------------------------------------------- /docs/assets/img/search.83621669.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fabula.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | foobar: 1, 4 | ssh: { 5 | stored: { 6 | host: 'stored', 7 | username: 'ubuntu', 8 | hostname: '3.80.152.37', 9 | privateKey: '/Users/jonas/Keys/galvez' 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/commands/index.js: -------------------------------------------------------------------------------- 1 | 2 | export default [ 3 | () => import('./handle'), 4 | () => import('./fabula'), 5 | () => import('./ensure'), 6 | () => import('./get'), 7 | () => import('./put'), 8 | () => import('./write'), 9 | () => import('./cd') 10 | ] 11 | -------------------------------------------------------------------------------- /test/fixtures/logging.fab: -------------------------------------------------------------------------------- 1 | local node ../bin.js --code 0 --stdout "<%= $server.$id %> success test" 2 | local node ../bin.js --code 1 --stderr "<%= $server.$id %> error test" 3 | bin.js --code 0 --stdout "<%= $server.$id %> success test" 4 | bin.js --code 1 --stderr "<%= $server.$id %> error test" 5 | -------------------------------------------------------------------------------- /bin/fabula.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const consola = require('consola') 4 | 5 | try { 6 | require('esm')(module)('../dist/fabula') 7 | .cli().catch((err) => { 8 | consola.fatal(err) 9 | process.exit(1) 10 | }) 11 | } catch (err) { 12 | consola.fatal(err) 13 | process.exit(1) 14 | } 15 | -------------------------------------------------------------------------------- /test/unit/__snapshots__/handling.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`test handling single handler 1`] = ` 4 | "[INFO] [local] [OK] write /tmp/fabula-handler-test: 5 | [INFO] [local] 1 6 | [INFO] [local] [OK] cat /tmp/fabula-handler-test 7 | [INFO] [local] [OK] rm /tmp/fabula-handler-test 8 | " 9 | `; 10 | -------------------------------------------------------------------------------- /src/commands/get.js: -------------------------------------------------------------------------------- 1 | import { get } from '../ssh' 2 | 3 | export default { 4 | match(line) { 5 | return line.trim().match(/^get\s+(.+)\s+(.+)/) 6 | }, 7 | line() { 8 | this.params.sourcePath = this.match[1] 9 | this.params.targetPath = this.match[2] 10 | }, 11 | command(conn) { 12 | return get(conn, this.params.sourcePath, this.param.targetPath) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/commands/put.js: -------------------------------------------------------------------------------- 1 | import { put } from '../ssh' 2 | 3 | export default { 4 | match(line) { 5 | return line.trim().match(/^put\s+(.+)\s+(.+)/) 6 | }, 7 | line() { 8 | this.params.sourcePath = this.match[1] 9 | this.params.targetPath = this.match[2] 10 | }, 11 | command(conn) { 12 | return put(conn, this.params.sourcePath, this.param.targetPath) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | 2 | // Ported from Eric S. Raymond's code at: 3 | // https://github.com/python/cpython/blob/master/Lib/shlex.py 4 | export function quote(s) { 5 | if (!s) { 6 | return "''" 7 | } 8 | s = s.replace(/\n/g, '\\n') 9 | // Use single quotes, and put single quotes into double quotes 10 | // the string $'b is then quoted as '$'"'"'b' 11 | s = s.replace(/'/g, "'\"'\"'") 12 | return `'${s}'` 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/handler.fab: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | fail: false, 4 | handle: (result) => { 5 | return { 6 | touchErrorCode: result.code 7 | } 8 | } 9 | } 10 | 11 | 12 | 13 | touch /parent/doesnt/exist @handle: 14 | local write /tmp/fabula-handler-test: 15 | <%= touchErrorCode %> 16 | cat /tmp/fabula-handler-test 17 | rm /tmp/fabula-handler-test 18 | 19 | -------------------------------------------------------------------------------- /test/fixtures/logs/ssh-log.log: -------------------------------------------------------------------------------- 1 | [server1] "server1 success test" 2 | [server1] [OK] bin.js --code 0 --stdout "server1 success test" 3 | [server2] "server2 success test" 4 | [server2] [OK] bin.js --code 0 --stdout "server2 success test" 5 | [server1] "server1 error test" 6 | [server1] [FAIL] bin.js --code 1 --stderr "server1 error test" 7 | [server2] "server2 error test" 8 | [server2] [FAIL] bin.js --code 1 --stderr "server2 error test" 9 | -------------------------------------------------------------------------------- /test/fixtures/simple.fab: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | env: { 4 | COMPONENT_VAR: 'COMPONENT_VAR' 5 | }, 6 | branch: 'my-branch', 7 | someFlag: true 8 | } 9 | 10 | 11 | 12 | cd ~ 13 | local mkdir -p foobar 14 | local git checkout <%= branch %> 15 | git checkout <%= branch %>-2 16 | local echo "foobarfobar" > foobar 17 | 18 | <% if (someFlag) { %> 19 | local touch /tmp/some-file 20 | <% } %> 21 | 22 | -------------------------------------------------------------------------------- /src/prompt.js: -------------------------------------------------------------------------------- 1 | import prompts from 'prompts' 2 | 3 | async function simplePrompt(message) { 4 | const result = await prompts({ 5 | name: 'value', 6 | type: 'text', 7 | message 8 | }) 9 | return result.value 10 | } 11 | 12 | export default function prompt(params) { 13 | if (typeof params === 'string') { 14 | return simplePrompt(params) 15 | } else if (typeof params === 'object') { 16 | return prompts(params) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docs/assets/js/2.d8f505d4.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[2],{165:function(t,n,e){},166:function(t,n,e){"use strict";var a=e(165);e.n(a).a},168:function(t,n,e){"use strict";e.r(n);var a={functional:!0,props:{type:{type:String,default:"tip"},text:String,vertical:{type:String,default:"top"}},render:function(t,n){var e=n.props,a=n.slots;return t("span",{class:["badge",e.type,e.vertical]},e.text||a().default)}},r=(e(166),e(0)),i=Object(r.a)(a,void 0,void 0,!1,null,"099ab69c",null);n.default=i.exports}}]); -------------------------------------------------------------------------------- /recipes/setup-local-ssh.fab: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | host: 'stored', 4 | hostname: '3.80.152.37', 5 | user: 'ubuntu', 6 | privateKey: '/Users/jonas/Keys/galvez' 7 | } 8 | 9 | 10 | 11 | Host <%= host %> 12 | Hostname <%= hostname %> 13 | User <%= user %> 14 | IdentityFile <%= privateKey %> 15 | StrictHostKeyChecking no 16 | 17 | 18 | 19 | local echo >> ~/.ssh/config # new line 20 | local append ~/.ssh/config strings.sshConfig 21 | 22 | 23 | -------------------------------------------------------------------------------- /test/util.js: -------------------------------------------------------------------------------- 1 | import { parse } from 'path' 2 | import { readFileSync, writeFileSync } from 'fs' 3 | import { compile } from '../dist/fabula' 4 | export { loadConfig } from '../dist/fabula' 5 | export { parseArgv } from '../dist/fabula' 6 | 7 | export async function compileForTest(source, config) { 8 | const name = parse(source).name 9 | if (!source.endsWith('.fab')) { 10 | source += '.fab' 11 | } 12 | source = readFileSync(source).toString() 13 | const [ commands, settings ] = await compile(name, source, config) 14 | return commands(settings) 15 | } 16 | -------------------------------------------------------------------------------- /.github/main.workflow: -------------------------------------------------------------------------------- 1 | workflow "CI" { 2 | on = "push" 3 | resolves = ["Install", "Audit", "Lint", "Test"] 4 | } 5 | 6 | action "Install" { 7 | uses = "nuxt/actions-yarn@master" 8 | args = "install --frozen-lockfile --non-interactive" 9 | } 10 | 11 | action "Audit" { 12 | uses = "nuxt/actions-yarn@master" 13 | args = "audit" 14 | } 15 | 16 | action "Lint" { 17 | uses = "nuxt/actions-yarn@master" 18 | needs = ["Install"] 19 | args = "lint" 20 | } 21 | 22 | action "Test" { 23 | uses = "actions/npm@master" 24 | needs = ["Install"] 25 | args = "test" 26 | } 27 | -------------------------------------------------------------------------------- /dist/fabula-get.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('fs'); 4 | require('consola'); 5 | require('util'); 6 | require('read'); 7 | require('ssh2'); 8 | const __chunk_1 = require('./fabula-chunk.js'); 9 | 10 | const get = { 11 | match(line) { 12 | return line.trim().match(/^get\s+(.+)\s+(.+)/) 13 | }, 14 | line() { 15 | this.params.sourcePath = this.match[1]; 16 | this.params.targetPath = this.match[2]; 17 | }, 18 | command(conn) { 19 | return __chunk_1.get(conn, this.params.sourcePath, this.param.targetPath) 20 | } 21 | }; 22 | 23 | exports.default = get; 24 | -------------------------------------------------------------------------------- /dist/fabula-put.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('fs'); 4 | require('consola'); 5 | require('util'); 6 | require('read'); 7 | require('ssh2'); 8 | const __chunk_1 = require('./fabula-chunk.js'); 9 | 10 | const put = { 11 | match(line) { 12 | return line.trim().match(/^put\s+(.+)\s+(.+)/) 13 | }, 14 | line() { 15 | this.params.sourcePath = this.match[1]; 16 | this.params.targetPath = this.match[2]; 17 | }, 18 | command(conn) { 19 | return __chunk_1.put(conn, this.params.sourcePath, this.param.targetPath) 20 | } 21 | }; 22 | 23 | exports.default = put; 24 | -------------------------------------------------------------------------------- /test/unit/handling.test.js: -------------------------------------------------------------------------------- 1 | import { writeFileSync, readFileSync } from 'fs' 2 | import { spawnSync } from 'child_process' 3 | import { resolve } from 'path' 4 | 5 | const opts = { cwd: resolve(__dirname, '../fixtures') } 6 | const fabulaBin = resolve(__dirname, '../../bin/fabula.js') 7 | const spawnFabula = (...args) => spawnSync(fabulaBin, args, opts) 8 | 9 | describe('test handling', () => { 10 | test('single handler', async () => { 11 | const { stdout } = spawnFabula('handler') 12 | expect(stdout.toString().replace(/\[\d.+?\] /g, '')).toMatchSnapshot() 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /test/fixtures/logs/local-log.log: -------------------------------------------------------------------------------- 1 | [local] "server1 success test" 2 | [local] [OK] node ../bin.js --code 0 --stdout "server1 success test" 3 | [local] "server2 success test" 4 | [local] [OK] node ../bin.js --code 0 --stdout "server2 success test" 5 | [local] "server1 error test" 6 | [local] [OK] node ../bin.js --code 1 --stderr "server1 error test" 7 | [local] "server2 error test" 8 | [local] [OK] node ../bin.js --code 1 --stderr "server2 error test" 9 | [local] [OK] write /tmp/fabula-handler-test: 10 | [local] 1 11 | [local] [OK] cat /tmp/fabula-handler-test 12 | [local] [OK] rm /tmp/fabula-handler-test 13 | -------------------------------------------------------------------------------- /src/commands/exec.js: -------------------------------------------------------------------------------- 1 | import { exec } from '../ssh' 2 | import { execLocal } from '../local' 3 | 4 | export default { 5 | prepend(prepend) { 6 | // Don't filter sudo from prepend for exec 7 | return prepend 8 | }, 9 | match(line) { 10 | if (this.local) { 11 | this.params.cmd = line.split(/^local\s+/)[1] 12 | } else { 13 | this.params.cmd = line 14 | } 15 | return true 16 | }, 17 | command(conn) { 18 | if (this.local) { 19 | return execLocal([this.argv[0], this.argv.slice(1)], this.env, this.settings.$cwd) 20 | } else { 21 | return exec(conn, this.params.cmd, this.env, this.settings.$cwd) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /recipes/setup-github-deployment.fab: -------------------------------------------------------------------------------- 1 | # This script runs on the remote instance 2 | # to add Github SSH configuration for deployment 3 | 4 | 5 | export default { 6 | github: { 7 | deployKey: '/path/to/deploy_key' 8 | } 9 | } 10 | 11 | 12 | 13 | mkdir -p /app/.keys 14 | chown -R ubuntu /app/.keys 15 | chmod 755 /app/.keys 16 | 17 | put <%= github.deploy_key %> /app/.keys/deploy_key 18 | chmod 400 /app/.keys/deploy_key 19 | 20 | echo /home/<%= conn.settings.user %>/.ssh/config: 21 | Host <%= host %> 22 | Hostname <%= hostname %> 23 | IdentityFile /app/.keys/deploy_key 24 | User git 25 | StrictHostKeyChecking no 26 | 27 | -------------------------------------------------------------------------------- /test/fixtures/keys/ssh.public: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDOKLkF65teH5NBp6KVvwWHKxK5aJBLY4rWBWFEEmBv8stfR6ZU9ie1ygdOsn+8+IHKCUghLTtfdQEwWZJTOQmW4XBAGGC+c/xCDbeAhcdcomtYP1jPE4JD6wAKSTzqaL5nbLqtSknCKuzCIxL8twn9PlGAfx4RboC00MsvpArHo3r9fkzvTxbpafyMm4f5H9iVp5St/tfSnnj/Wa6v4NesmZE9kFED64UNmyAbTDd7KiJS/qHzkYESdLn5H7l4Eqjzm6BNbhBoPyyXev7BwxbkKL/d9t5mYK0vaSK676TyrKm3bnLOW71eUg3o326hqL0Gv7CumrP4/Hu55fzRNFYT7WPaVDrvtNqpB6Ss3CwVYnzUTzROvcRG3v96avAmw0uj9PB+7vZgAl/tcy/N34/ebuGPzPZvQ4pRqDi0BXQ/2z51Mr8lFnsgVb3rsPi/3vcaxlFxxERgmYlW+oUKaLgiVYvKfs409BNDZ92McqX+Ou6GNRBaT73YdRAMTGOfpQgV7C6DIdLVkpMB9qlbQWnRFRTgpQLYKPJJ5XvuOVwq6Mm7h/tFYPhxwXKSEUUiyHMQtticn54j/pyySuwQr5FbCIJJgVfjfntEzpNVvu0J5yAQWEwfb4CLHNaq9fP8VL9NI9NIUphpnx0FyLZuL0MqPlc10RQ8+XPnGqBQdnuHmw== fabula-test-ssh-server 2 | -------------------------------------------------------------------------------- /docs/logging.md: -------------------------------------------------------------------------------- 1 | # Logging 2 | 3 | Logging can be configured in a fashion similar to environment variables: global, 4 | local, remote, per server and per component. 5 | 6 | [consola]: https://github.com/nuxt/consola 7 | 8 | ## Top level 9 | 10 | ```js 11 | export default { 12 | logs: { 13 | global: 'logs/global.log', 14 | local: 'logs/local.log', 15 | ssh: 'logs/ssh.log' 16 | }, 17 | } 18 | ``` 19 | 20 | ## Per server 21 | 22 | ```js 23 | export default { 24 | ssh: { 25 | server1: { 26 | hostname: '1.2.3.4', 27 | username: 'serveruser', 28 | log: 'logs/ssh-server1.log' 29 | } 30 | } 31 | } 32 | ``` 33 | 34 | ## Per component 35 | 36 | ```js 37 | 38 | export default { 39 | log: 'logs/component.log' 40 | } 41 | 42 | ``` 43 | -------------------------------------------------------------------------------- /src/commands/fabula.js: -------------------------------------------------------------------------------- 1 | import { readFile } from 'fs' 2 | import { runSource, runLocalSource } from '../run' 3 | 4 | export default { 5 | match(line) { 6 | this.dedent = 0 7 | let match 8 | // eslint-disable-next-line no-cond-assign 9 | if (match = line.match(/^fabula\s+\.\s+([^ ]+?)\s*$/)) { 10 | this.params.filePath = this.match[1] 11 | return match 12 | } 13 | }, 14 | async command(conn, logger) { 15 | const settings = { 16 | ...this.settings, 17 | fail: true 18 | } 19 | const commands = readFile(this.params.filePath).toString() 20 | if (this.local) { 21 | await runLocalSource(this.settings.$name, commands, settings, logger) 22 | } else { 23 | await runSource(this.context.server, conn, this.settings.$name, commands, settings, logger) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | To install, use [`npm`][npm] or [`yarn`][yarn]: 4 | 5 | ```sh 6 | $ npm install fabula -g 7 | $ yarn global add fabula 8 | ``` 9 | 10 | ## Highlighting 11 | 12 | As new and experimental as **Fabula** is, there's no code editor or IDE plugin available providing specialized syntax highlighting. For the moment, just set 13 | your editor's syntax settings to **XML** when editing **Fabula** files. 14 | 15 | ## Disclaimer 16 | 17 | **Fabula** is currently in **beta phase**, that is to say, we're still testing 18 | on several projects and making sure all potential issues are addressed. Don't 19 | use it in production just yet unless you're sure what you're doing. And please 20 | [file an issue on Github][gh-issue] if you find anything that needs attention. 21 | 22 | [npm]: https://www.npmjs.com/ 23 | [yarn]: https://yarnpkg.com/ 24 | [gh-issue]: https://github.com/nuxt/fabula/issues/new -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | expand: true, 4 | forceExit: true, 5 | 6 | // https://github.com/facebook/jest/pull/6747 fix warning here 7 | // But its performance overhead is pretty bad (30+%). 8 | // detectOpenHandles: true 9 | 10 | // setupTestFrameworkScriptFile: './test/utils/setup', 11 | // coverageDirectory: './coverage', 12 | // collectCoverageFrom: [], 13 | // coveragePathIgnorePatterns: [ 14 | // 'node_modules/(?!(@nuxt|nuxt))', 15 | // 'packages/webpack/src/config/plugins/vue' 16 | // ], 17 | testPathIgnorePatterns: [ 18 | 'bin/', 19 | 'node_modules/', 20 | 'test/fixtures/.*/.*?/' 21 | ], 22 | 23 | transform: { 24 | '^.+\\.js$': 'babel-jest' 25 | }, 26 | 27 | moduleFileExtensions: [ 28 | 'js', 29 | 'json' 30 | ], 31 | 32 | reporters: [ 33 | 'default' 34 | ].concat(process.env.JEST_JUNIT_OUTPUT ? ['jest-junit'] : []) 35 | } 36 | -------------------------------------------------------------------------------- /test/fixtures/logs/global-log.log: -------------------------------------------------------------------------------- 1 | [local] "server1 success test" 2 | [local] [OK] node ../bin.js --code 0 --stdout "server1 success test" 3 | [local] "server2 success test" 4 | [local] [OK] node ../bin.js --code 0 --stdout "server2 success test" 5 | [local] "server1 error test" 6 | [local] [OK] node ../bin.js --code 1 --stderr "server1 error test" 7 | [local] "server2 error test" 8 | [local] [OK] node ../bin.js --code 1 --stderr "server2 error test" 9 | [server1] "server1 success test" 10 | [server1] [OK] bin.js --code 0 --stdout "server1 success test" 11 | [server2] "server2 success test" 12 | [server2] [OK] bin.js --code 0 --stdout "server2 success test" 13 | [server1] "server1 error test" 14 | [server1] [FAIL] bin.js --code 1 --stderr "server1 error test" 15 | [server2] "server2 error test" 16 | [server2] [FAIL] bin.js --code 1 --stderr "server2 error test" 17 | [local] [OK] write /tmp/fabula-handler-test: 18 | [local] 1 19 | [local] [OK] cat /tmp/fabula-handler-test 20 | [local] [OK] rm /tmp/fabula-handler-test 21 | -------------------------------------------------------------------------------- /test/bin.js: -------------------------------------------------------------------------------- 1 | async function main (shell = true, argv = process.argv) { 2 | const args = require('arg')({ 3 | '--code': Number, 4 | '--stdout': String, 5 | '--stderr': String, 6 | }, { argv }), 7 | // eslint-disable-next-line no-eval 8 | stdout = args['--stdout'], 9 | // eslint-disable-next-line no-eval 10 | stderr = args['--stderr'], 11 | result = { 12 | code: args['--code'] || 0 13 | } 14 | 15 | if (stdout) { 16 | result.stdout = `${stdout}\n` 17 | } 18 | if (stderr) { 19 | result.stderr = `${stderr}\n` 20 | } else if (result.code) { 21 | result.stderr = `exit code: ${result.code}\n` 22 | } 23 | if (!shell) { 24 | return result 25 | } 26 | 27 | if (result.stdout) { 28 | process.stdout.write(result.stdout) 29 | } 30 | if (result.stderr || result.code) { 31 | process.stderr.write(result.stderr) 32 | } 33 | return result 34 | } 35 | 36 | module.exports = main 37 | 38 | if (require.main === module) { 39 | main().then() // (result) => process.exit(result.code)) 40 | } 41 | -------------------------------------------------------------------------------- /test/fixtures/advanced.fab: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | k8s: { 4 | clusterName: 'my-cluster' 5 | }, 6 | files: [ 7 | { name: 'file1', contents: 'Contents \nof file1' }, 8 | { name: 'file2', contents: 'Contents \' of file2' } 9 | ] 10 | } 11 | 12 | 13 | 14 | local cd ~ 15 | local mkdir -p test 16 | write /tmp/test: 17 | goes into the file 18 | 19 | sudo service nginx restart 20 | 21 | # write will resolve its third argument 22 | # to the settings object (settings.files[index] etc) 23 | <% for (const file in files) { %> 24 | local write /tmp/<%= files[file].name %> files[<%= file %>].contents 25 | <% } %> 26 | 27 | # quote() prepares a string for the shell, 28 | # wrapping it in a single quote if needed and 29 | # escaping all unsafe characters 30 | <% for (const file of files) { %> 31 | local echo <%= quote(file.contents) %> > /tmp/<%= file.name %> 32 | <% } %> 33 | 34 | # Lines ending in \ continue into the next line (like Bash) 35 | gcloud container clusters create <%= k8s.clusterName %> \ 36 | --machine-type=n1-standard-2 \ 37 | --zone=southamerica-east1-a \ 38 | --num-nodes=4 39 | 40 | ls test 41 | 42 | -------------------------------------------------------------------------------- /test/fixtures/fabula.js: -------------------------------------------------------------------------------- 1 | const { launchTestSSHServer } = require('../server') 2 | 3 | function reporter (info) { 4 | return `${info.args.join(' ')}\n` 5 | } 6 | 7 | export default async function() { 8 | const [ 9 | server1, 10 | server2 11 | ] = await Promise.all([ 12 | launchTestSSHServer(), 13 | launchTestSSHServer() 14 | ]) 15 | return { 16 | ssh: { 17 | server1: { 18 | log: { path: 'logs/server1-log.log', reporter }, 19 | ...server1.settings 20 | }, 21 | server2: { 22 | log: { path: 'logs/server2-log.log', reporter }, 23 | ...server2.settings 24 | } 25 | }, 26 | done() { 27 | server1.server.close() 28 | server2.server.close() 29 | }, 30 | logs: { 31 | global: { path: 'logs/global-log.log', reporter }, 32 | local: { path: 'logs/local-log.log', reporter }, 33 | ssh: { path: 'logs/ssh-log.log', reporter } 34 | }, 35 | env: { 36 | GLOBAL_VAR: 'GLOBAL_VAR', 37 | local: { 38 | GLOBAL_LOCAL_VAR: 'GLOBAL_LOCAL_VAR' 39 | }, 40 | ssh: { 41 | GLOBAL_SSH_VAR: 'GLOBAL_SSH_VAR' 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/local.js: -------------------------------------------------------------------------------- 1 | import { writeFileSync, appendFileSync } from 'fs' 2 | import { spawn } from 'child_process' 3 | 4 | export function execLocal(cmd, env = {}, cwd = null) { 5 | return new Promise((resolve) => { 6 | let stdout = '' 7 | let stderr = '' 8 | const options = { 9 | env: { ...process.env, ...env }, 10 | cwd: cwd || process.cwd() 11 | } 12 | 13 | const stream = spawn(...cmd, options) 14 | stream.on('error', err => resolve(err)) 15 | stream.stdout.on('data', (data) => { stdout += data }) 16 | stream.stderr.on('data', (data) => { stderr += data }) 17 | stream.on('exit', (code) => { 18 | resolve({ stdout, stderr, code }) 19 | }) 20 | }) 21 | } 22 | 23 | export function localWrite(filePath, fileContents) { 24 | try { 25 | writeFileSync(filePath, fileContents) 26 | return { code: 0 } 27 | } catch (err) { 28 | return { code: 1, stderr: err.message || err.toString() } 29 | } 30 | } 31 | 32 | export function localAppend(filePath, fileContents) { 33 | try { 34 | appendFileSync(filePath, fileContents) 35 | return { code: 0 } 36 | } catch (err) { 37 | return { code: 1, stderr: err.message || err.toString() } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /dist/fabula-fabula.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | require('path'); 5 | require('consola'); 6 | require('lodash.merge'); 7 | require('util'); 8 | require('read'); 9 | require('ssh2'); 10 | require('./fabula-chunk.js'); 11 | require('module'); 12 | require('os'); 13 | require('lodash.template'); 14 | const __chunk_2 = require('./fabula-chunk2.js'); 15 | require('child_process'); 16 | require('./fabula-chunk3.js'); 17 | require('prompts'); 18 | 19 | const fabula = { 20 | match(line) { 21 | this.dedent = 0; 22 | let match; 23 | // eslint-disable-next-line no-cond-assign 24 | if (match = line.match(/^fabula\s+\.\s+([^ ]+?)\s*$/)) { 25 | this.params.filePath = this.match[1]; 26 | return match 27 | } 28 | }, 29 | async command(conn, logger) { 30 | const settings = { 31 | ...this.settings, 32 | fail: true 33 | }; 34 | const commands = fs.readFile(this.params.filePath).toString(); 35 | if (this.local) { 36 | await __chunk_2.runLocalSource(this.settings.$name, commands, settings, logger); 37 | } else { 38 | await __chunk_2.runSource(this.context.server, conn, this.settings.$name, commands, settings, logger); 39 | } 40 | } 41 | }; 42 | 43 | exports.default = fabula; 44 | -------------------------------------------------------------------------------- /docs/authentication.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | 3 | To configure access for servers, you must either ensure your local `ssh-agent` 4 | is running, or provide custom authentication settings for each server. If using 5 | an encrypted key, providing `privateKey` in addition to `hostname` and 6 | `username` usually suffices (`port` can also be set and defaults to `22`): 7 | 8 | ## Private key 9 | 10 | ```js 11 | export default { 12 | ssh: { 13 | server: { 14 | hostname: '1.2.3.4', 15 | privateKey: '/path/to/key' 16 | } 17 | } 18 | } 19 | ``` 20 | 21 | Setting `privateKey` will skip `ssh-agent` and the authentication will be 22 | handled by **Fabula**. If you're using an encrypted key, you can provide the 23 | `passphrase` option, or you'll be automatically prompted for one when a task 24 | runs (recommended for safety). 25 | 26 | ## SSH agent 27 | 28 | If you fail to provide `privateKey`, **Fabula** will assume it should use the 29 | local `ssh-agent` and **will automatically use `process.env.SSH_AUTH_SOCK`**. 30 | 31 | You can override it by setting `agent` in `fabula.js`: 32 | 33 | ```js 34 | export default { 35 | agent: process.env.CUSTOM_SSH_AUTH_SOCK, 36 | ssh: { 37 | server: { 38 | hostname: '1.2.3.4', 39 | } 40 | } 41 | } 42 | ``` 43 | -------------------------------------------------------------------------------- /dist/fabula-chunk3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const child_process = require('child_process'); 5 | 6 | function execLocal(cmd, env = {}, cwd = null) { 7 | return new Promise((resolve) => { 8 | let stdout = ''; 9 | let stderr = ''; 10 | const options = { 11 | env: { ...process.env, ...env }, 12 | cwd: cwd || process.cwd() 13 | }; 14 | 15 | const stream = child_process.spawn(...cmd, options); 16 | stream.on('error', err => resolve(err)); 17 | stream.stdout.on('data', (data) => { stdout += data; }); 18 | stream.stderr.on('data', (data) => { stderr += data; }); 19 | stream.on('exit', (code) => { 20 | resolve({ stdout, stderr, code }); 21 | }); 22 | }) 23 | } 24 | 25 | function localWrite(filePath, fileContents) { 26 | try { 27 | fs.writeFileSync(filePath, fileContents); 28 | return { code: 0 } 29 | } catch (err) { 30 | return { code: 1, stderr: err.message || err.toString() } 31 | } 32 | } 33 | 34 | function localAppend(filePath, fileContents) { 35 | try { 36 | fs.appendFileSync(filePath, fileContents); 37 | return { code: 0 } 38 | } catch (err) { 39 | return { code: 1, stderr: err.message || err.toString() } 40 | } 41 | } 42 | 43 | exports.execLocal = execLocal; 44 | exports.localAppend = localAppend; 45 | exports.localWrite = localWrite; 46 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: 'Fabula', 3 | description: 'Minimalist server configuration and task management.', 4 | base: '/fabula/', 5 | themeConfig: { 6 | repo: 'nuxt/fabula', 7 | editLinks: true, 8 | docsDir: 'docs', 9 | locales: { 10 | '/': { 11 | label: 'English', 12 | selectText: 'Languages', 13 | editLinkText: 'Edit this page on GitHub', 14 | sidebar: { 15 | '/': sidebarLinks('en') 16 | } 17 | }, 18 | } 19 | } 20 | } 21 | 22 | function sidebarLinks (locale) { 23 | const translations = { 24 | en: { 25 | groups: { 26 | main: 'Guide' 27 | }, 28 | 'page/': 'Introduction' 29 | } 30 | } 31 | 32 | const localePageTitle = (page) => { 33 | if (translations[locale][`page/${page}`]) { 34 | return [page, translations[locale][`page/${page}`]] 35 | } 36 | return page 37 | } 38 | 39 | const toc = [ 40 | { 41 | title: translations[locale].groups.main, 42 | collapsable: false, 43 | children: [ 44 | '', 45 | 'installation', 46 | 'environment', 47 | 'logging', 48 | 'components', 49 | 'commands', 50 | 'failure', 51 | 'authentication' 52 | ].map(child => localePageTitle(child)) 53 | } 54 | ] 55 | 56 | return toc 57 | } 58 | -------------------------------------------------------------------------------- /recipes/install-nginx-on-ubuntu.fab: -------------------------------------------------------------------------------- 1 | 2 | # From https://do.co/2OcRBq4 3 | # How To Install Nginx on Ubuntu 18.04 4 | # And register an example.com website afterwards 5 | 6 | 7 | export default { 8 | env: { 9 | DEBIAN_FRONTEND: 'noninteractive' 10 | }, 11 | domain: 'example.com', 12 | dirs: { 13 | html: '/var/www/${domain}/html' 14 | conf: '/var/www/${domain}/conf' 15 | } 16 | } 17 | 18 | 19 | 20 | apt update -y 21 | apt install nginx -y 22 | mkdir -p <%= dirs.html %> 23 | chown -R <%= user %>:<%= user %> <%= dirs.html %> 24 | chmod -R 755 <%= dirs.html %> 25 | write <%= dirs.html %> strings['nginx.html'] 26 | write <%= dirs.conf %> strings['nginx.conf'] 27 | ln -s <%= dirs.conf %> /etc/nginx/sites-enabled/<%= domain %> 28 | systemctl reload nginx 29 | 30 | 31 | 32 | 33 | 34 | Welcome to Example.com! 35 | 36 | 37 |

Success! The example.com server block is working!

38 | 39 | 40 |
41 | 42 | 43 | server { 44 | listen 80; 45 | listen [::]:80; 46 | 47 | root /var/www/<%= domain %>/html; 48 | index index.html; 49 | server_name <%= domain %> www.<%= domain %>; 50 | location / { 51 | try_files $uri $uri/ =404; 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /docs/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Fabula 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

404

Looks like we've got some broken links.
Take me home.
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

Fabula 3 | 4 |

5 | Minimalist server configuration and task management. 6 |

7 | 8 | Go straight to the [full documentation][docs] if you'd like. 9 | 10 | Or read the [introductory blog post][post]. 11 | 12 | [post]: https://hire.jonasgalvez.com.br/2019/may/05/a-vuejs-inspired-task-runner/ 13 | 14 | # Introduction 15 | 16 | At its core, **Fabula** is a simple Bash script preprocessor and runner. It lets 17 | you run scripts **locally** and on **remote servers**. **Fabula** (latin for 18 | _story_) is inspired by Python's [Fabric][f]. 19 | 20 | [f]: https://www.fabfile.org/ 21 | 22 | ```xml 23 | 24 | export default { 25 | docsDir: { 26 | local: './docs', 27 | remote: '/remote/path/www' 28 | } 29 | } 30 | 31 | 32 | 33 | local vuepress build <%= docsDir.local %> 34 | put <%= docsDir.local %>/.vuepress/dist/ <%= docsDir.remote %> 35 | sudo service nginx restart 36 | 37 | ``` 38 | 39 | Inspired by Vue, it lets you keep settings and commands in concise **single-file components**. 40 | 41 | Please refer to the [full documentation][docs] to learn more. 42 | 43 | [docs]: https://galvez.github.io/fabula/ 44 | 45 | ## Meta 46 | 47 | Created by [Jonas Galvez][jg]. 48 | 49 | [jg]: http://hire.jonasgalvez.com.br 50 | -------------------------------------------------------------------------------- /docs/environment.md: -------------------------------------------------------------------------------- 1 | # Environment 2 | 3 | Environment variables can bet set in various ways. 4 | 5 | ## Global 6 | 7 | To set environment variables globally for both local and remote settings, 8 | assign keys to the `env` object in **Fabula**'s configuration file: 9 | 10 | ```js 11 | export default { 12 | env: { 13 | FOOBAR: 'foobar' 14 | } 15 | } 16 | ``` 17 | 18 | Note, however, that the keys `local` and `ssh` are reserved. 19 | 20 | ## Local 21 | 22 | Use `env.local` in **Fabula**'s configuration file (`fabula.js`): 23 | 24 | ```js 25 | export default { 26 | env: { 27 | local: { 28 | FOOBAR: 'foobar' 29 | } 30 | } 31 | } 32 | ``` 33 | 34 | ## Remote 35 | 36 | Use `env.ssh` in **Fabula**'s configuration file (`fabula.js`): 37 | 38 | ```js 39 | export default { 40 | env: { 41 | ssh: { 42 | FOOBAR: 'foobar' 43 | } 44 | } 45 | } 46 | ``` 47 | 48 | This sets environment variables for all remote servers. These variables are used 49 | in **every remote command**, from any **Fabula** task file. 50 | 51 | ## Per server 52 | 53 | You may also place `env` underneath each SSH server: 54 | 55 | ```js 56 | export default { 57 | ssh: { 58 | server1: { 59 | hostname: '1.2.3.4', 60 | privateKey: '/path/to/key', 61 | env: { 62 | FOOBAR: 'foobar' 63 | } 64 | } 65 | } 66 | } 67 | ``` 68 | 69 | Environment variables set for a remote server are used for every command that 70 | is ran on that server from any **Fabula** task file. 71 | 72 | ## Per component 73 | 74 | ```js 75 | 76 | export default { 77 | env: { 78 | FOOBAR: 'foobar' 79 | } 80 | } 81 | 82 | ``` 83 | 84 | Environment variables set in a **Fabula** component are only used for the 85 | commands contained in it, both local and remote. 86 | -------------------------------------------------------------------------------- /test/unit/logging.test.js: -------------------------------------------------------------------------------- 1 | import { writeFileSync, readFileSync } from 'fs' 2 | import { spawnSync } from 'child_process' 3 | import { resolve } from 'path' 4 | 5 | const logs = [ 6 | 'global', 7 | 'local', 8 | 'ssh', 9 | 'server1', 10 | 'server2' 11 | ].reduce((obj, log) => { 12 | const logPath = `../fixtures/logs/${log}-log.log`.split('/') 13 | return { ...obj, [log]: resolve(__dirname, ...logPath) } 14 | }, {}) 15 | 16 | const opts = { cwd: resolve(__dirname, '../fixtures') } 17 | const fabulaBin = resolve(__dirname, '../../bin/fabula.js') 18 | const spawnFabula = (...args) => spawnSync(fabulaBin, args, opts) 19 | 20 | function extractLogs(stdout, stderr) { 21 | stdout = stdout.split(/\n/) 22 | stderr = stderr.split(/\n/) 23 | return { 24 | global: stdout, 25 | local: stdout.filter(line => line.match(/\[local\]/)), 26 | ssh: stdout.filter(line => line.match(/\[server\d\]/)), 27 | server1: stdout.filter(line => line.match(/\[server1\]/)), 28 | server2: stdout.filter(line => line.match(/\[server2\]/)), 29 | } 30 | } 31 | 32 | describe('test logging', () => { 33 | test('all logs', async () => { 34 | await Promise.all( 35 | Object.keys(logs).map((log) => new Promise((resolve) => { 36 | writeFileSync(logs[log], '') 37 | resolve() 38 | })) 39 | ) 40 | const { stdout, stderr } = spawnFabula('all', 'logging') 41 | const output = extractLogs( 42 | stdout.toString().trim(), 43 | stderr.toString().trim() 44 | ) 45 | const compareLogs = (raw, msg) => { 46 | raw.split(/\n/).forEach((line, i) => { 47 | expect(msg[i]).toContain(line) 48 | }) 49 | } 50 | for (const log in logs) { 51 | compareLogs(readFileSync(logs[log]).toString().trim(), output[log]) 52 | } 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fabula", 3 | "version": "0.2.0", 4 | "description": "Minimalist server configuration and task management", 5 | "author": "Jonas Galvez ", 6 | "license": "MIT", 7 | "engines": { 8 | "node": ">= 10.0.0" 9 | }, 10 | "bin": { 11 | "fabula": "./bin/fabula.js" 12 | }, 13 | "main": "dist/fabula.js", 14 | "files": [ 15 | "bin", 16 | "dist" 17 | ], 18 | "scripts": { 19 | "build": "rollup -c build/rollup.config.js", 20 | "test": "jest --detectOpenHandles --runInBand", 21 | "lint": "eslint --fix --ext .js,.vue src", 22 | "docs:dev": "vuepress dev docs", 23 | "docs:build": "source deploy.sh" 24 | }, 25 | "dependencies": { 26 | "arg": "3.0.0", 27 | "consola": "2.3.0", 28 | "esm": "^3.2.25", 29 | "js-yaml": "^3.13.1", 30 | "lodash": "^4.17.15", 31 | "lodash.template": "^4.5.0", 32 | "mem": "^5.1.1", 33 | "prompts": "^2.0.4", 34 | "read": "1.0.7", 35 | "ssh2": "0.6.1" 36 | }, 37 | "devDependencies": { 38 | "@nuxtjs/eslint-config": "0.0.1", 39 | "babel-eslint": "^10.0.1", 40 | "babel-jest": "^23.6.0", 41 | "babel-preset-env": "^1.7.0", 42 | "buffer-equal-constant-time": "^1.0.1", 43 | "eslint": "^5.9.0", 44 | "eslint-config-standard": "^12.0.0", 45 | "eslint-plugin-import": "^2.14.0", 46 | "eslint-plugin-jest": "^22.0.0", 47 | "eslint-plugin-node": "^8.0.0", 48 | "eslint-plugin-promise": "^4.0.1", 49 | "eslint-plugin-standard": "^4.0.0", 50 | "eslint-plugin-vue": "^4.7.1", 51 | "ip": "^1.1.5", 52 | "jest": "^23.6.0", 53 | "rollup": "^1.10.0", 54 | "rollup-plugin-alias": "^1.5.1", 55 | "rollup-plugin-babel": "^4.3.2", 56 | "rollup-plugin-commonjs": "^9.3.4", 57 | "rollup-plugin-json": "^4.0.0", 58 | "rollup-plugin-license": "^0.8.1", 59 | "rollup-plugin-node-resolve": "^4.2.3", 60 | "rollup-plugin-replace": "^2.2.0", 61 | "vuepress": "0.14.8" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/commands/cd.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { runSource, runLocalSource } from '../run' 3 | 4 | export default { 5 | name: 'cd', 6 | patterns: { 7 | block: /^(?:local\s*)?cd\s+(.+?):\s*$/, 8 | global: /^(?:local\s*)?cd\s+([^ :]+?)\s*$/ 9 | }, 10 | match(line) { 11 | this.dedent = 0 12 | if (this.argv[0] === 'cd') { 13 | let match 14 | // eslint-disable-next-line no-cond-assign 15 | if (match = line.match(this.cmd.patterns.block)) { 16 | this.block = true 17 | return match 18 | // eslint-disable-next-line no-cond-assign 19 | } else if (match = line.match(this.cmd.patterns.global)) { 20 | this.global = true 21 | return match 22 | } 23 | } 24 | }, 25 | line(line) { 26 | if (this.firstLine) { 27 | if (this.global) { 28 | this.settings.$cwd = this.match[1] 29 | return false 30 | } else { 31 | this.params.cwd = this.match[1] 32 | this.params.commands = [] 33 | return true 34 | } 35 | } else if (!/^\s+/.test(line)) { 36 | return false 37 | } else { 38 | if (this.params.commands.length === 0) { 39 | const match = line.match(/^\s+/) 40 | if (match) { 41 | this.dedent = match[0].length 42 | } 43 | } 44 | this.params.commands.push(line.slice(this.dedent)) 45 | return true 46 | } 47 | }, 48 | async command(conn, logger) { 49 | const settings = { 50 | ...this.settings, 51 | $cwd: resolve( 52 | this.settings.$cwd || process.cwd(), 53 | this.params.cwd 54 | ) 55 | } 56 | const commands = this.params.commands.map((cmd) => { 57 | if (this.local && !/^\s+/.test(cmd)) { 58 | cmd = `local ${cmd}` 59 | } 60 | return cmd 61 | }).join('\n') 62 | if (this.local) { 63 | await runLocalSource(this.settings.$name, commands, settings, logger) 64 | } else { 65 | await runSource(this.context.server, conn, this.settings.$name, commands, settings, logger) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/commands/ensure.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { runSource, runLocalSource } from '../run' 3 | 4 | export default { 5 | name: 'ensure', 6 | patterns: { 7 | block: /^(?:local\s*)?ensure\s+(.+?):\s*$/, 8 | global: /^(?:local\s*)?ensure\s+([^ :]+?)\s*$/ 9 | }, 10 | match(line) { 11 | this.dedent = 0 12 | if (this.argv[0] === 'ensure') { 13 | let match 14 | // eslint-disable-next-line no-cond-assign 15 | if (match = line.match(this.cmd.patterns.block)) { 16 | this.block = true 17 | return match 18 | // eslint-disable-next-line no-cond-assign 19 | } else if (match = line.match(this.cmd.patterns.global)) { 20 | this.global = true 21 | return match 22 | } 23 | } 24 | }, 25 | line(line) { 26 | if (this.firstLine) { 27 | if (this.global) { 28 | this.settings.$cwd = this.match[1] 29 | return false 30 | } else { 31 | this.params.cwd = this.match[1] 32 | this.params.commands = [] 33 | return true 34 | } 35 | } else if (!/^\s+/.test(line)) { 36 | return false 37 | } else { 38 | if (this.params.commands.length === 0) { 39 | const match = line.match(/^\s+/) 40 | if (match) { 41 | this.dedent = match[0].length 42 | } 43 | } 44 | this.params.commands.push(line.slice(this.dedent)) 45 | return true 46 | } 47 | }, 48 | async command(conn, logger) { 49 | const settings = { 50 | ...this.settings, 51 | fail: true, 52 | $cwd: resolve( 53 | this.settings.$cwd || process.cwd(), 54 | this.params.cwd 55 | ) 56 | } 57 | const commands = this.params.commands.map((cmd) => { 58 | if (this.local && !/^\s+/.test(cmd)) { 59 | cmd = `local ${cmd}` 60 | } 61 | return cmd 62 | }).join('\n') 63 | if (this.local) { 64 | await runLocalSource(this.settings.$name, commands, settings, logger) 65 | } else { 66 | await runSource(this.context.server, conn, this.settings.$name, commands, settings, logger) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/commands/write.js: -------------------------------------------------------------------------------- 1 | import { write, append } from '../ssh' 2 | import { localWrite, localAppend } from '../local' 3 | 4 | export default { 5 | patterns: { 6 | block: (argv) => { 7 | return new RegExp(`^(?:local\\s*)?${argv[0]}\\s+(.+?):$`) 8 | }, 9 | string: (argv) => { 10 | return new RegExp(`^(?:local\\s*)?${argv[0]}\\s+([^ ]+?)\\s+([^ :]+?)$`) 11 | } 12 | }, 13 | match(line) { 14 | this.op = this.argv[0] 15 | this.dedent = 0 16 | if (['append', 'write'].includes(this.argv[0])) { 17 | let match 18 | // eslint-disable-next-line no-cond-assign 19 | if (match = line.match(this.cmd.patterns.block(this.argv))) { 20 | this.block = true 21 | return match 22 | // eslint-disable-next-line no-cond-assign 23 | } else if (match = line.match(this.cmd.patterns.string(this.argv))) { 24 | this.string = true 25 | return match 26 | } 27 | } 28 | }, 29 | line(line) { 30 | if (this.firstLine) { 31 | this.params.filePath = this.match[1] 32 | this.params.fileContents = [] 33 | if (this.string) { 34 | const settingsKey = this.match[2] 35 | this.params.fileContents = () => { 36 | // eslint-disable-next-line no-eval 37 | return eval(`this.settings.${settingsKey}`).split(/\n/g) 38 | } 39 | return false 40 | } else { 41 | return true 42 | } 43 | } else if (!/^\s+/.test(line)) { 44 | return false 45 | } else { 46 | if (this.params.fileContents.length === 0) { 47 | const match = line.match(/^\s+/) 48 | if (match) { 49 | this.dedent = match[0].length 50 | } 51 | } 52 | this.params.fileContents.push(line.slice(this.dedent)) 53 | return true 54 | } 55 | }, 56 | command(conn) { 57 | const filePath = this.params.filePath 58 | const fileContents = typeof this.params.fileContents === 'function' 59 | ? this.params.fileContents() 60 | : this.params.fileContents.join('\n') 61 | if (this.local) { 62 | const cmd = ({ write: localWrite, append: localAppend })[this.op] 63 | return cmd(filePath, fileContents) 64 | } else { 65 | return ({ write, append })[this.op](conn, filePath, fileContents) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /docs/assets/js/9.dca480d7.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[9],{175:function(t,e,n){"use strict";n.r(e);var s=n(0),a=Object(s.a)({},function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"content"},[t._m(0),t._v(" "),n("p",[t._v("To install, use "),n("a",{attrs:{href:"https://www.npmjs.com/",target:"_blank",rel:"noopener noreferrer"}},[n("code",[t._v("npm")]),n("OutboundLink")],1),t._v(" or "),n("a",{attrs:{href:"https://yarnpkg.com/",target:"_blank",rel:"noopener noreferrer"}},[n("code",[t._v("yarn")]),n("OutboundLink")],1),t._v(":")]),t._v(" "),t._m(1),t._m(2),t._v(" "),t._m(3),t._v(" "),t._m(4),t._v(" "),n("p",[n("strong",[t._v("Fabula")]),t._v(" is currently in "),n("strong",[t._v("beta phase")]),t._v(", that is to say, we're still testing\non several projects and making sure all potential issues are addressed. Don't\nuse it in production just yet unless you're sure what you're doing. And please\n"),n("a",{attrs:{href:"https://github.com/nuxt/fabula/issues/new",target:"_blank",rel:"noopener noreferrer"}},[t._v("file an issue on Github"),n("OutboundLink")],1),t._v(" if you find anything that needs attention.")])])},[function(){var t=this.$createElement,e=this._self._c||t;return e("h1",{attrs:{id:"installation"}},[e("a",{staticClass:"header-anchor",attrs:{href:"#installation","aria-hidden":"true"}},[this._v("#")]),this._v(" Installation")])},function(){var t=this.$createElement,e=this._self._c||t;return e("div",{staticClass:"language-sh extra-class"},[e("pre",{pre:!0,attrs:{class:"language-text"}},[e("code",[this._v("$ npm install fabula -g\n$ yarn global add fabula\n")])])])},function(){var t=this.$createElement,e=this._self._c||t;return e("h2",{attrs:{id:"highlighting"}},[e("a",{staticClass:"header-anchor",attrs:{href:"#highlighting","aria-hidden":"true"}},[this._v("#")]),this._v(" Highlighting")])},function(){var t=this.$createElement,e=this._self._c||t;return e("p",[this._v("As new and experimental as "),e("strong",[this._v("Fabula")]),this._v(" is, there's no code editor or IDE plugin available providing specialized syntax highlighting. For the moment, just set\nyour editor's syntax settings to "),e("strong",[this._v("XML")]),this._v(" when editing "),e("strong",[this._v("Fabula")]),this._v(" files.")])},function(){var t=this.$createElement,e=this._self._c||t;return e("h2",{attrs:{id:"disclaimer"}},[e("a",{staticClass:"header-anchor",attrs:{href:"#disclaimer","aria-hidden":"true"}},[this._v("#")]),this._v(" Disclaimer")])}],!1,null,null,null);e.default=a.exports}}]); -------------------------------------------------------------------------------- /dist/fabula-cd.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('fs'); 4 | const path = require('path'); 5 | require('consola'); 6 | require('lodash.merge'); 7 | require('util'); 8 | require('read'); 9 | require('ssh2'); 10 | require('./fabula-chunk.js'); 11 | require('module'); 12 | require('os'); 13 | require('lodash.template'); 14 | const __chunk_2 = require('./fabula-chunk2.js'); 15 | require('child_process'); 16 | require('./fabula-chunk3.js'); 17 | require('prompts'); 18 | 19 | const cd = { 20 | name: 'cd', 21 | patterns: { 22 | block: /^(?:local\s*)?cd\s+(.+?):\s*$/, 23 | global: /^(?:local\s*)?cd\s+([^ :]+?)\s*$/ 24 | }, 25 | match(line) { 26 | this.dedent = 0; 27 | if (this.argv[0] === 'cd') { 28 | let match; 29 | // eslint-disable-next-line no-cond-assign 30 | if (match = line.match(this.cmd.patterns.block)) { 31 | this.block = true; 32 | return match 33 | // eslint-disable-next-line no-cond-assign 34 | } else if (match = line.match(this.cmd.patterns.global)) { 35 | this.global = true; 36 | return match 37 | } 38 | } 39 | }, 40 | line(line) { 41 | if (this.firstLine) { 42 | if (this.global) { 43 | this.settings.$cwd = this.match[1]; 44 | return false 45 | } else { 46 | this.params.cwd = this.match[1]; 47 | this.params.commands = []; 48 | return true 49 | } 50 | } else if (!/^\s+/.test(line)) { 51 | return false 52 | } else { 53 | if (this.params.commands.length === 0) { 54 | const match = line.match(/^\s+/); 55 | if (match) { 56 | this.dedent = match[0].length; 57 | } 58 | } 59 | this.params.commands.push(line.slice(this.dedent)); 60 | return true 61 | } 62 | }, 63 | async command(conn, logger) { 64 | const settings = { 65 | ...this.settings, 66 | $cwd: path.resolve( 67 | this.settings.$cwd || process.cwd(), 68 | this.params.cwd 69 | ) 70 | }; 71 | const commands = this.params.commands.map((cmd) => { 72 | if (this.local && !/^\s+/.test(cmd)) { 73 | cmd = `local ${cmd}`; 74 | } 75 | return cmd 76 | }).join('\n'); 77 | if (this.local) { 78 | await __chunk_2.runLocalSource(this.settings.$name, commands, settings, logger); 79 | } else { 80 | await __chunk_2.runSource(this.context.server, conn, this.settings.$name, commands, settings, logger); 81 | } 82 | } 83 | }; 84 | 85 | exports.default = cd; 86 | -------------------------------------------------------------------------------- /dist/fabula-ensure.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('fs'); 4 | const path = require('path'); 5 | require('consola'); 6 | require('lodash.merge'); 7 | require('util'); 8 | require('read'); 9 | require('ssh2'); 10 | require('./fabula-chunk.js'); 11 | require('module'); 12 | require('os'); 13 | require('lodash.template'); 14 | const __chunk_2 = require('./fabula-chunk2.js'); 15 | require('child_process'); 16 | require('./fabula-chunk3.js'); 17 | require('prompts'); 18 | 19 | const ensure = { 20 | name: 'ensure', 21 | patterns: { 22 | block: /^(?:local\s*)?ensure\s+(.+?):\s*$/, 23 | global: /^(?:local\s*)?ensure\s+([^ :]+?)\s*$/ 24 | }, 25 | match(line) { 26 | this.dedent = 0; 27 | if (this.argv[0] === 'ensure') { 28 | let match; 29 | // eslint-disable-next-line no-cond-assign 30 | if (match = line.match(this.cmd.patterns.block)) { 31 | this.block = true; 32 | return match 33 | // eslint-disable-next-line no-cond-assign 34 | } else if (match = line.match(this.cmd.patterns.global)) { 35 | this.global = true; 36 | return match 37 | } 38 | } 39 | }, 40 | line(line) { 41 | if (this.firstLine) { 42 | if (this.global) { 43 | this.settings.$cwd = this.match[1]; 44 | return false 45 | } else { 46 | this.params.cwd = this.match[1]; 47 | this.params.commands = []; 48 | return true 49 | } 50 | } else if (!/^\s+/.test(line)) { 51 | return false 52 | } else { 53 | if (this.params.commands.length === 0) { 54 | const match = line.match(/^\s+/); 55 | if (match) { 56 | this.dedent = match[0].length; 57 | } 58 | } 59 | this.params.commands.push(line.slice(this.dedent)); 60 | return true 61 | } 62 | }, 63 | async command(conn, logger) { 64 | const settings = { 65 | ...this.settings, 66 | fail: true, 67 | $cwd: path.resolve( 68 | this.settings.$cwd || process.cwd(), 69 | this.params.cwd 70 | ) 71 | }; 72 | const commands = this.params.commands.map((cmd) => { 73 | if (this.local && !/^\s+/.test(cmd)) { 74 | cmd = `local ${cmd}`; 75 | } 76 | return cmd 77 | }).join('\n'); 78 | if (this.local) { 79 | await __chunk_2.runLocalSource(this.settings.$name, commands, settings, logger); 80 | } else { 81 | await __chunk_2.runSource(this.context.server, conn, this.settings.$name, commands, settings, logger); 82 | } 83 | } 84 | }; 85 | 86 | exports.default = ensure; 87 | -------------------------------------------------------------------------------- /src/commands/handle.js: -------------------------------------------------------------------------------- 1 | 2 | import merge from 'lodash.merge' 3 | import prompt from '../prompt' 4 | import { exec } from '../ssh' 5 | import { execLocal } from '../local' 6 | import { runSource, runLocalSource } from '../run' 7 | 8 | export default { 9 | name: 'handle', 10 | patterns: { 11 | block: /^(?:local\s*)?(.+?)\s*@([\w\d_]+):\s*$/, 12 | global: /^(?:local\s*)?(.+?)\s*@([\w\d_]+)\s*$/ 13 | }, 14 | match(line) { 15 | this.dedent = 0 16 | let match 17 | // eslint-disable-next-line no-cond-assign 18 | if (match = line.match(this.cmd.patterns.block)) { 19 | this.block = true 20 | this.params.cmd = match[1] 21 | this.handler = match[2] 22 | return match 23 | // eslint-disable-next-line no-cond-assign 24 | } else if (match = line.match(this.cmd.patterns.global)) { 25 | this.global = true 26 | this.params.cmd = match[1] 27 | this.handler = match[2] 28 | return match 29 | } 30 | }, 31 | line(line) { 32 | if (this.firstLine) { 33 | if (this.global) { 34 | return false 35 | } else { 36 | this.params.commands = [] 37 | return true 38 | } 39 | } else if (!/^\s+/.test(line)) { 40 | return false 41 | } else { 42 | if (this.params.commands.length === 0) { 43 | const match = line.match(/^\s+/) 44 | if (match) { 45 | this.dedent = match[0].length 46 | } 47 | } 48 | this.params.commands.push(line.slice(this.dedent)) 49 | return true 50 | } 51 | }, 52 | async command(conn, logger) { 53 | const settings = { ...this.settings } 54 | let result 55 | if (this.local) { 56 | this.argv.pop() 57 | result = await execLocal([this.argv[0], this.argv.slice(1)], this.env, this.settings.$cwd) 58 | } else { 59 | result = await exec(conn, this.params.cmd, this.env, this.settings.$cwd) 60 | } 61 | let abort = false 62 | const fabula = { 63 | prompt, 64 | abort: () => { 65 | abort = true 66 | } 67 | } 68 | if (this.handler && this.settings[this.handler]) { 69 | merge(settings, await this.settings[this.handler](result, fabula)) 70 | } 71 | if (abort) { 72 | return false 73 | } 74 | const commands = this.params.commands.join('\n') 75 | if (this.context.server) { 76 | await runSource(this.context.server, conn, this.settings.$name, commands, settings, logger) 77 | } else { 78 | await runLocalSource(this.settings.$name, commands, settings, logger) 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /dist/fabula-write.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('fs'); 4 | require('consola'); 5 | require('util'); 6 | require('read'); 7 | require('ssh2'); 8 | const __chunk_1 = require('./fabula-chunk.js'); 9 | require('child_process'); 10 | const __chunk_3 = require('./fabula-chunk3.js'); 11 | 12 | const write = { 13 | patterns: { 14 | block: (argv) => { 15 | return new RegExp(`^(?:local\\s*)?${argv[0]}\\s+(.+?):$`) 16 | }, 17 | string: (argv) => { 18 | return new RegExp(`^(?:local\\s*)?${argv[0]}\\s+([^ ]+?)\\s+([^ :]+?)$`) 19 | } 20 | }, 21 | match(line) { 22 | this.op = this.argv[0]; 23 | this.dedent = 0; 24 | if (['append', 'write'].includes(this.argv[0])) { 25 | let match; 26 | // eslint-disable-next-line no-cond-assign 27 | if (match = line.match(this.cmd.patterns.block(this.argv))) { 28 | this.block = true; 29 | return match 30 | // eslint-disable-next-line no-cond-assign 31 | } else if (match = line.match(this.cmd.patterns.string(this.argv))) { 32 | this.string = true; 33 | return match 34 | } 35 | } 36 | }, 37 | line(line) { 38 | if (this.firstLine) { 39 | this.params.filePath = this.match[1]; 40 | this.params.fileContents = []; 41 | if (this.string) { 42 | const settingsKey = this.match[2]; 43 | this.params.fileContents = () => { 44 | // eslint-disable-next-line no-eval 45 | return eval(`this.settings.${settingsKey}`).split(/\n/g) 46 | }; 47 | return false 48 | } else { 49 | return true 50 | } 51 | } else if (!/^\s+/.test(line)) { 52 | return false 53 | } else { 54 | if (this.params.fileContents.length === 0) { 55 | const match = line.match(/^\s+/); 56 | if (match) { 57 | this.dedent = match[0].length; 58 | } 59 | } 60 | this.params.fileContents.push(line.slice(this.dedent)); 61 | return true 62 | } 63 | }, 64 | command(conn) { 65 | const filePath = this.params.filePath; 66 | const fileContents = typeof this.params.fileContents === 'function' 67 | ? this.params.fileContents() 68 | : this.params.fileContents.join('\n'); 69 | if (this.local) { 70 | const cmd = ({ write: __chunk_3.localWrite, append: __chunk_3.localAppend })[this.op]; 71 | return cmd(filePath, fileContents) 72 | } else { 73 | return ({ write: __chunk_1.write, append: __chunk_1.append })[this.op](conn, filePath, fileContents) 74 | } 75 | } 76 | }; 77 | 78 | exports.default = write; 79 | -------------------------------------------------------------------------------- /src/ssh.js: -------------------------------------------------------------------------------- 1 | 2 | import { readFileSync } from 'fs' 3 | import { promisify } from 'util' 4 | import consola from 'consola' 5 | import read from 'read' 6 | import { Client } from 'ssh2' 7 | 8 | function exit(err) { 9 | consola.fatal(err.message || err) 10 | process.exit() 11 | } 12 | 13 | function askPassphrase(privateKey) { 14 | return new Promise((resolve) => { 15 | const prompt = { 16 | prompt: `${privateKey} requires a passphrase: `, 17 | silent: true, 18 | replace: '*' 19 | } 20 | read(prompt, (_, passphrase) => { 21 | resolve(passphrase) 22 | }) 23 | }) 24 | } 25 | 26 | function isKeyEncrypted(privateKey) { 27 | if (/^-*BEGIN ENCRYPTED PRIVATE KEY/g.test(privateKey)) { 28 | return true 29 | } else { 30 | const firstLines = privateKey.split(/\n/g).slice(0, 2) 31 | return firstLines.some(line => line.match(/Proc-Type: 4,ENCRYPTED/)) 32 | } 33 | } 34 | 35 | export function getConnection(settings) { 36 | return new Promise(async (resolve, reject) => { 37 | const conn = new Client() 38 | conn.exec = promisify(conn.exec).bind(conn) 39 | conn.sftp = promisify(conn.sftp).bind(conn) 40 | conn.on('error', exit) 41 | conn.on('ready', () => { 42 | conn.settings = settings 43 | resolve(conn) 44 | }) 45 | 46 | let connect 47 | if (settings.privateKey) { 48 | const privateKey = readFileSync(settings.privateKey).toString() 49 | if (!settings.passphrase && isKeyEncrypted(privateKey)) { 50 | settings.passphrase = await askPassphrase(settings.privateKey) 51 | } 52 | connect = () => conn.connect({ ...settings, privateKey }) 53 | } else { 54 | if (!settings.agent) { 55 | settings.agent = process.env.SSH_AUTH_SOCK 56 | } 57 | connect = () => conn.connect({ ...settings }) 58 | } 59 | await connect() 60 | }) 61 | } 62 | 63 | export async function write(conn, filePath, fileContents) { 64 | const stream = await conn.sftp() 65 | return stream.writeFile(filePath, fileContents) 66 | } 67 | 68 | export async function append(conn, filePath, fileContents) { 69 | const stream = await conn.sftp() 70 | return stream.appendFile(filePath, fileContents) 71 | } 72 | 73 | export async function get(conn, remotePath, localPath) { 74 | const stream = await conn.sftp() 75 | return stream.fastGet(remotePath, localPath) 76 | } 77 | 78 | export async function put(conn, localPath, remotePath) { 79 | const stream = await conn.sftp() 80 | return stream.fastPut(localPath, remotePath) 81 | } 82 | 83 | export function exec(conn, cmd, env = {}) { 84 | return new Promise(async (resolve, reject) => { 85 | let stdout = '' 86 | let stderr = '' 87 | const stream = await conn.exec(cmd, { env }) 88 | stream.on('close', (code, signal) => { 89 | resolve({ stdout, stderr, code, signal }) 90 | }) 91 | stream.on('data', (data) => { stdout += data }) 92 | stream.stderr.on('data', (data) => { stderr += data }) 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | 2 | import { existsSync } from 'fs' 3 | import { resolve } from 'path' 4 | import consola from 'consola' 5 | import arg from 'arg' 6 | import { run } from './run' 7 | import { active } from './logging' 8 | 9 | function resolvePath(path) { 10 | return resolve(process.cwd(), ...path.split('/')) 11 | } 12 | 13 | function activeStreams() { 14 | return new Promise((resolve) => { 15 | setTimeout(() => { 16 | if (!active) { 17 | resolve() 18 | } else { 19 | resolve(activeStreams()) 20 | } 21 | }, 1) 22 | }) 23 | } 24 | 25 | // const fabulaConfigHelper = () => { 26 | 27 | // } 28 | 29 | export async function loadConfig(rcFile = null) { 30 | let config 31 | if (rcFile === null) { 32 | for (rcFile of ['fabula.js', '.fabularc.js', '.fabularc']) { 33 | if (existsSync(resolvePath(rcFile))) { 34 | config = require(resolvePath(rcFile)) 35 | break 36 | } 37 | rcFile = null 38 | } 39 | } else { 40 | config = require(rcFile) 41 | } 42 | if (rcFile === null) { 43 | consola.warn('No Fabula configuration file found.') 44 | config = {} 45 | } 46 | config = config.default || config 47 | if (typeof config === 'function') { 48 | config = await config() 49 | } 50 | return config 51 | } 52 | 53 | function showHelpAndExit() { 54 | process.stdout.write( 55 | '\n' + 56 | ' Usage: fabula (run on specified servers)\n' + 57 | ' fabula all (run on all servers)\n' + 58 | ' fabula (run local only)\n\n' 59 | ) 60 | process.exit() 61 | } 62 | 63 | function ensureSource(source) { 64 | if (!source.endsWith('.fab')) { 65 | source = `${source}.fab` 66 | } 67 | if (!existsSync(source)) { 68 | consola.fatal(`Task source doesn't exist: ${source}.`) 69 | process.exit() 70 | } 71 | return source 72 | } 73 | 74 | export default async function () { 75 | const args = arg({}) 76 | if (args._.length === 0 || args._[0] === 'help') { 77 | showHelpAndExit() 78 | } 79 | const config = await loadConfig() 80 | if (args._.length === 2) { 81 | // Run on remote servers: 82 | // fabula 34 | 35 | 36 | -------------------------------------------------------------------------------- /docs/authentication.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Authentication | Fabula 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

Authentication

To configure access for servers, you must either ensure your local ssh-agent 19 | is running, or provide custom authentication settings for each server. If using 20 | an encrypted key, providing privateKey in addition to hostname and 21 | username usually suffices (port can also be set and defaults to 22):

Private key

export default {
22 |   ssh: {
23 |   	server: {
24 |       hostname: '1.2.3.4',
25 |       privateKey: '/path/to/key'
26 |   	}
27 |   }
28 | }
29 | 

Setting privateKey will skip ssh-agent and the authentication will be 30 | handled by Fabula. If you're using an encrypted key, you can provide the 31 | passphrase option, or you'll be automatically prompted for one when a task 32 | runs (recommended for safety).

SSH agent

If you fail to provide privateKey, Fabula will assume it should use the 33 | local ssh-agent and will automatically use process.env.SSH_AUTH_SOCK.

You can override it by setting agent in fabula.js:

export default {
34 |   agent: process.env.CUSTOM_SSH_AUTH_SOCK,
35 |   ssh: {
36 |   	server: {
37 |       hostname: '1.2.3.4',
38 |   	}
39 |   }
40 | }
41 | 
46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /docs/logging.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Logging | Fabula 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

Logging

Logging can be configured in a fashion similar to environment variables: global, 19 | local, remote, per server and per component.

Top level

export default {
20 |   logs: {
21 |     global: 'logs/global.log',
22 |     local: 'logs/local.log',
23 |     ssh: 'logs/ssh.log'
24 |   },
25 | }
26 | 

Per server

export default {
27 |   ssh: {
28 |     server1: {
29 |       hostname: '1.2.3.4',
30 |       username: 'serveruser',
31 |       log: 'logs/ssh-server1.log'
32 |     }
33 |   }
34 | }
35 | 

Per component

<fabula>
36 | export default {
37 |   log: 'logs/component.log'
38 | }
39 | </fabula>
40 | 
49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /docs/assets/js/7.e7d4a4a6.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[7],{173:function(t,a,s){"use strict";s.r(a);var n=s(0),e=Object(n.a)({},function(){this.$createElement;this._self._c;return this._m(0)},[function(){var t=this,a=t.$createElement,s=t._self._c||a;return s("div",{staticClass:"content"},[s("h1",{attrs:{id:"environment"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#environment","aria-hidden":"true"}},[t._v("#")]),t._v(" Environment")]),t._v(" "),s("p",[t._v("Environment variables can bet set in various ways.")]),t._v(" "),s("h2",{attrs:{id:"global"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#global","aria-hidden":"true"}},[t._v("#")]),t._v(" Global")]),t._v(" "),s("p",[t._v("To set environment variables globally for both local and remote settings,\nassign keys to the "),s("code",[t._v("env")]),t._v(" object in "),s("strong",[t._v("Fabula")]),t._v("'s configuration file:")]),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("export")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("default")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n env"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token constant"}},[t._v("FOOBAR")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'foobar'")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n")])])]),s("p",[t._v("Note, however, that the keys "),s("code",[t._v("local")]),t._v(" and "),s("code",[t._v("ssh")]),t._v(" are reserved.")]),t._v(" "),s("h2",{attrs:{id:"local"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#local","aria-hidden":"true"}},[t._v("#")]),t._v(" Local")]),t._v(" "),s("p",[t._v("Use "),s("code",[t._v("env.local")]),t._v(" in "),s("strong",[t._v("Fabula")]),t._v("'s configuration file ("),s("code",[t._v("fabula.js")]),t._v("):")]),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("export")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("default")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n env"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n \tlocal"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token constant"}},[t._v("FOOBAR")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'foobar'")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n")])])]),s("h2",{attrs:{id:"remote"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#remote","aria-hidden":"true"}},[t._v("#")]),t._v(" Remote")]),t._v(" "),s("p",[t._v("Use "),s("code",[t._v("env.ssh")]),t._v(" in "),s("strong",[t._v("Fabula")]),t._v("'s configuration file ("),s("code",[t._v("fabula.js")]),t._v("):")]),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("export")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("default")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n env"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n \tssh"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token constant"}},[t._v("FOOBAR")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'foobar'")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n")])])]),s("p",[t._v("This sets environment variables for all remote servers. These variables are used\nin "),s("strong",[t._v("every remote command")]),t._v(", from any "),s("strong",[t._v("Fabula")]),t._v(" task file.")]),t._v(" "),s("h2",{attrs:{id:"per-server"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#per-server","aria-hidden":"true"}},[t._v("#")]),t._v(" Per server")]),t._v(" "),s("p",[t._v("You may also place "),s("code",[t._v("env")]),t._v(" underneath each SSH server:")]),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("export")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("default")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n ssh"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n server1"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n hostname"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'1.2.3.4'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n privateKey"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'/path/to/key'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n env"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token constant"}},[t._v("FOOBAR")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'foobar'")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n")])])]),s("p",[t._v("Environment variables set for a remote server are used for every command that\nis ran on that server from any "),s("strong",[t._v("Fabula")]),t._v(" task file.")]),t._v(" "),s("h2",{attrs:{id:"per-component"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#per-component","aria-hidden":"true"}},[t._v("#")]),t._v(" Per component")]),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("<")]),t._v("fabula"),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(">")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("export")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("default")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n env"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token constant"}},[t._v("FOOBAR")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'foobar'")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("<")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("/")]),t._v("fabula"),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(">")]),t._v("\n")])])]),s("p",[t._v("Environment variables set in a "),s("strong",[t._v("Fabula")]),t._v(" component are only used for the\ncommands contained in it, both local and remote.")])])}],!1,null,null,null);a.default=e.exports}}]); -------------------------------------------------------------------------------- /docs/commands.md: -------------------------------------------------------------------------------- 1 | # Commands 2 | 3 | As stated in the introduction, **every command available to the underlying Bash 4 | shell** will work in a **Fabula** task. There are however a few convenience 5 | commands that are specific to **Fabula**. 6 | 7 | ## Local 8 | 9 | Every command preceded by `local` will run on the local machine: 10 | 11 | ```sh 12 | local mkdir -p /tmp/foobar 13 | local touch /tmp/foobar 14 | ``` 15 | 16 | ## Append 17 | 18 | Appends a block text or string to the file in the specified path. 19 | 20 | > Availability: `local` and `remote` 21 | 22 | ### Simple use 23 | 24 | ```sh 25 | local append /path/to/file: 26 | multi-line contents 27 | to be appended to the file 28 | ``` 29 | 30 | Text will be automatically dedented to the number of total white 31 | spaces in the **first line**. 32 | 33 | ### With string id 34 | 35 | ```sh 36 | 37 | export default { 38 | path: '/path/to/file ' 39 | } 40 | 41 | 42 | 43 | local append <%= path %> strings.contents 44 | 45 | 46 | 47 | multi-line contents 48 | to be appended to the file 49 | 50 | ``` 51 | 52 | ## Write 53 | 54 | Writes a block text or string to the file in the specified path. 55 | 56 | > Availability: `local` and `remote` 57 | 58 | This command has essentially the same semantics of `append`, with the difference 59 | that it will never append to, but rather overwrite the contents of the target entirely. 60 | 61 | 62 | ```sh 63 | local write /path/to/file: 64 | multi-line contents 65 | to be written to the file 66 | ``` 67 | 68 | ## Get 69 | 70 | Copies file at path on the remote server to path on the local machine. 71 | 72 | > Availability: `remote` 73 | 74 | ```sh 75 | get /path/on/remote/server /path/to/local/file 76 | ``` 77 | 78 | ## Put 79 | 80 | Copies file at path on the local machine to path on the remote server. 81 | 82 | > Availability: `remote` 83 | 84 | ```sh 85 | put /path/to/local/file /path/on/remote/server 86 | ``` 87 | 88 | ## Custom 89 | 90 | To make the bash script parser as flexible and fault-tolerant as possible, 91 | **Fabula** introduces a simple, straight-forward compiler with an API for writing 92 | command handlers. The special `put` built-in command for instance, is 93 | defined under `src/commands/put.js`: 94 | 95 | ```js 96 | import { put } from '../ssh' 97 | 98 | export default { 99 | match(line) { 100 | return line.trim().match(/^put\s+(.+)\s+(.+)/) 101 | }, 102 | line() { 103 | this.params.sourcePath = this.match[1] 104 | this.params.targetPath = this.match[2] 105 | }, 106 | command(conn) { 107 | return put(conn, this.params.sourcePath, this.param.targetPath) 108 | } 109 | } 110 | ``` 111 | 112 | - `match()` is called once for every new line, if no previous command is still 113 | being parsed. If `match()` returns `true`, `line()` will run for the current 114 | and every subsequent line as long as you keep returning `true`, which means, 115 | _continue parsing lines for the **current command**_. 116 | 117 | - When `line()` returns `false` or `undefined`, the compiler understands the 118 | current command is **done parsing** and moves on. 119 | 120 | - with `line()`, we can store data that is retrieved from each line in the 121 | command block, make it availble under `this.params` and later access it when 122 | actually calling `command()` (done automatically when running scripts). 123 | 124 | ## Registration 125 | 126 | Say you want to register the command `special `, that can run only on the 127 | local machine. You can add a custom command handler to your `fabula.js` 128 | configuration file under `commands`: 129 | 130 | ```js 131 | export default { 132 | commands: [ 133 | { 134 | match(line) { 135 | this.local = true 136 | const match = line.trim().match(/^special\s+(.+)/) 137 | this.params.arg = match[1] 138 | return match 139 | }, 140 | command(conn) { 141 | return { stdout: `From special command: ${this.params.arg}!` } 142 | } 143 | } 144 | ] 145 | } 146 | ``` 147 | 148 | Note that you could also use an external module: 149 | 150 | ```js 151 | import specialCommand from './customCommand' 152 | 153 | export default { 154 | commands: [ specialCommand ] 155 | } 156 | ``` 157 | 158 | If you have a `task.fab` file with `special foobar`, its output will be: 159 | 160 | ```sh 161 | ℹ [local] From special command: foobar! 162 | ℹ [local] [OK] special foobar 163 | ``` 164 | 165 | Note that you have successfuly defined a local command that can be ran without 166 | being preceded by `local`. That is because you **manually** set it to `local` 167 | in `match()`. You can use `match()` to determine if the command is local or not 168 | and still make it work both ways. **Fabula**'s built-in `write` and `append` are 169 | good examples of this and the subject of the next topic. 170 | 171 | ### Advanced example 172 | 173 | ```sh 174 | local write /path/to/file: 175 | contents 176 | local write /path/to/file string.id 177 | write /path/to/file: 178 | contents 179 | write /path/to/file string.id 180 | local append /path/to/file: 181 | contents 182 | local append /path/to/file string.id 183 | append /path/to/file string.id 184 | ``` 185 | 186 | The snippet above contains commands that are handled by [the same internal Fabula 187 | code](https://github.com/nuxt/fabula/blob/master/src/commands/write.js). Let's 188 | take a quick dive into how it works. 189 | 190 | ```js 191 | import { write, append } from '../ssh' 192 | import { localWrite, localAppend } from '../local' 193 | 194 | export default { 195 | patterns: { 196 | block: (argv) => { 197 | return new RegExp(`^(?:local\\s*)?${argv[0]}\\s+(.+?):$`) 198 | }, 199 | string: (argv) => { 200 | return new RegExp(`^(?:local\\s*)?${argv[0]}\\s+([^ ]+?)\\s+([^ :]+?)$`) 201 | } 202 | }, 203 | match(line) { 204 | const argv = [...this.argv] 205 | if (argv[0] === 'local') { 206 | argv.shift() 207 | this.local = true 208 | } 209 | this.op = argv[0] 210 | this.dedent = 0 211 | if (['append', 'write'].includes(argv[0])) { 212 | let match 213 | // eslint-disable-next-line no-cond-assign 214 | if (match = line.match(this.cmd.patterns.block(argv))) { 215 | this.block = true 216 | return match 217 | // eslint-disable-next-line no-cond-assign 218 | } else if (match = line.match(this.cmd.patterns.string(argv))) { 219 | this.string = true 220 | return match 221 | } 222 | } 223 | }, 224 | ``` 225 | 226 | First we import all necessary dependencies and define `match()`, which uses 227 | two kinds of patterns for matching the command: one is for dedented blocks of 228 | text (`patterns.block`) and other for string references (`patterns.string`). 229 | `match()` also sets the `local` attribute for the command. 230 | 231 | ```js 232 | line(line) { 233 | if (this.firstLine) { 234 | this.params.filePath = this.match[1] 235 | this.params.fileContents = '' 236 | if (this.string) { 237 | const settingsKey = this.match[2] 238 | // eslint-disable-next-line no-eval 239 | this.params.fileContents = eval(`this.settings.${settingsKey}`) 240 | return false 241 | } else { 242 | return true 243 | } 244 | } else if (!/^\s+/.test(line)) { 245 | this.params.fileContents = this.params.fileContents.replace(/\n$/g, '') 246 | return false 247 | } else { 248 | if (this.params.fileContents.length === 0) { 249 | const match = line.match(/^\s+/) 250 | if (match) { 251 | this.dedent = match[0].length 252 | } 253 | } 254 | this.params.fileContents += `${line.slice(this.dedent)}\n` 255 | return true 256 | } 257 | }, 258 | ``` 259 | 260 | The magic happens in `line()`, which will continue parsing the command in 261 | subsequent lines if it's a block of text, or use the provided string reference. 262 | We store the provided text in `fileContents`, which is then retrieved by `command()`. 263 | 264 | ```js 265 | command(conn) { 266 | const filePath = this.params.filePath 267 | const fileContents = this.params.fileContents 268 | if (this.local) { 269 | const cmd = ({ write: localWrite, append: localAppend })[this.op] 270 | return cmd(filePath, fileContents) 271 | } else { 272 | return ({ write, append })[this.op](conn, filePath, fileContents) 273 | } 274 | } 275 | ``` 276 | 277 | As **Fabula** evolves, the code for this command and underlying functions it 278 | calls will likely change, but the API for defining and parsing the commands is 279 | likely to stay the same as dissecated in this article. 280 | -------------------------------------------------------------------------------- /docs/environment.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Environment | Fabula 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

Environment

Environment variables can bet set in various ways.

Global

To set environment variables globally for both local and remote settings, 19 | assign keys to the env object in Fabula's configuration file:

export default {
20 |   env: {
21 |     FOOBAR: 'foobar'
22 |   }
23 | }
24 | 

Note, however, that the keys local and ssh are reserved.

Local

Use env.local in Fabula's configuration file (fabula.js):

export default {
25 |   env: {
26 |   	local: {
27 |       FOOBAR: 'foobar'
28 |     }
29 |   }
30 | }
31 | 

Remote

Use env.ssh in Fabula's configuration file (fabula.js):

export default {
32 |   env: {
33 |   	ssh: {
34 |       FOOBAR: 'foobar'
35 |     }
36 |   }
37 | }
38 | 

This sets environment variables for all remote servers. These variables are used 39 | in every remote command, from any Fabula task file.

Per server

You may also place env underneath each SSH server:

export default {
40 |   ssh: {
41 |     server1: {
42 |       hostname: '1.2.3.4',
43 |       privateKey: '/path/to/key',
44 |       env: {
45 |         FOOBAR: 'foobar'
46 |       }
47 |     }
48 |   }
49 | }
50 | 

Environment variables set for a remote server are used for every command that 51 | is ran on that server from any Fabula task file.

Per component

<fabula>
52 | export default {
53 |   env: {
54 |     FOOBAR: 'foobar'
55 |   }
56 | }
57 | </fabula>
58 | 

Environment variables set in a Fabula component are only used for the 59 | commands contained in it, both local and remote.

68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /docs/assets/js/8.c52cc8a3.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[8],{174:function(t,a,s){"use strict";s.r(a);var e=s(0),n=Object(e.a)({},function(){var t=this,a=t.$createElement,s=t._self._c||a;return s("div",{staticClass:"content"},[t._m(0),t._v(" "),t._m(1),t._v(" "),t._m(2),t._m(3),t._v(" "),t._m(4),t._v(" "),t._m(5),t._v(" "),t._m(6),t._m(7),t._v(" "),t._m(8),t._v(" "),t._m(9),t._m(10),t._v(" "),t._m(11),t._v(" "),s("p",[t._v("Take "),s("a",{attrs:{href:"https://github.com/nuxt/fabula/blob/master/test/fixtures/handler.fab",target:"_blank",rel:"noopener noreferrer"}},[t._v("this example"),s("OutboundLink")],1),t._v(" from the test suite fixtures.")]),t._v(" "),t._m(12),t._m(13),t._v(" "),t._m(14),t._v(" "),t._m(15),t._v(" "),t._m(16),s("p",[t._v("Or, alternatively, grouped in a block:")]),t._v(" "),t._m(17),s("p",[t._v("Note that if you use "),s("code",[t._v("")]),t._v(" with a "),s("router-link",{attrs:{to:"./components.html#prepend"}},[t._v("prepend command")]),t._v(" all commands\nwithin "),s("code",[t._v("ensure")]),t._v(" will be altered, but not "),s("code",[t._v("ensure")]),t._v(" itself.")],1),t._v(" "),t._m(18),t._v(" "),t._m(19),t._v(" "),t._m(20)])},[function(){var t=this.$createElement,a=this._self._c||t;return a("h1",{attrs:{id:"failure"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#failure","aria-hidden":"true"}},[this._v("#")]),this._v(" Failure")])},function(){var t=this.$createElement,a=this._self._c||t;return a("p",[this._v("By default, a single failing command will cause "),a("strong",[this._v("Fabula")]),this._v(" to exit and prevent\nany subsequent commands or tasks from running. You can disable this behaviour in "),a("strong",[this._v("Fabula")]),this._v("'s configuration file:")])},function(){var t=this,a=t.$createElement,s=t._self._c||a;return s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("export")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("default")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n fail"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token boolean"}},[t._v("false")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n")])])])},function(){var t=this.$createElement,a=this._self._c||t;return a("p",[this._v("You can also set "),a("code",[this._v("fail: false")]),this._v(" on a "),a("strong",[this._v("Fabula")]),this._v(" component.")])},function(){var t=this.$createElement,a=this._self._c||t;return a("h2",{attrs:{id:"handlers"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#handlers","aria-hidden":"true"}},[this._v("#")]),this._v(" Handlers")])},function(){var t=this.$createElement,a=this._self._c||t;return a("p",[this._v("You can handle results for individual commands as well. A common example is\nhandling a "),a("code",[this._v("yarn install")]),this._v(" result. If "),a("code",[this._v("fail")]),this._v(" is set to "),a("code",[this._v("false")]),this._v(", you may want\nto handle the result of certain commands.")])},function(){var t=this,a=t.$createElement,s=t._self._c||a;return s("div",{staticClass:"language-xml extra-class"},[s("pre",{pre:!0,attrs:{class:"language-xml"}},[s("code",[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("<")]),t._v("fabula")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(">")])]),t._v("\nexport default {\n fail: false\n}\n"),s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("")])]),t._v("\n\n"),s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("<")]),t._v("commands")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(">")])]),t._v("\nunimportant command 1\nunimportant command 2\nyarn install\nyarn build\n"),s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("")])]),t._v("\n")])])])},function(){var t=this.$createElement,a=this._self._c||t;return a("p",[this._v("In the above script, you would want to ensure "),a("code",[this._v("yarn install")]),this._v(" finished\nsuccesfully before moving on to "),a("code",[this._v("yarn build")]),this._v(", even though you don't care about\nthe first two unimportant commands.")])},function(){var t=this.$createElement,a=this._self._c||t;return a("p",[a("strong",[this._v("Fabula")]),this._v(" lets you tag an individual command line and set a "),a("em",[this._v("callback")]),this._v(" matching\nthe tag given:")])},function(){var t=this,a=t.$createElement,s=t._self._c||a;return s("div",{staticClass:"language-xml extra-class"},[s("pre",{pre:!0,attrs:{class:"language-xml"}},[s("code",[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("<")]),t._v("fabula")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(">")])]),t._v("\nexport default {\n fail: false,\n check({ code, stderr }, fabula) {\n if (code) {\n fabula.abort()\n }\n }\n}\n"),s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("")])]),t._v("\n\n"),s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("<")]),t._v("commands")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(">")])]),t._v("\nunimportant command 1\nunimportant command 2\nyarn install @check\nyarn build\n"),s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("")])]),t._v("\n")])])])},function(){var t=this,a=t.$createElement,s=t._self._c||a;return s("p",[t._v("You can tag a command by placing a label prefixed with "),s("strong",[s("code",[t._v("@")])]),t._v(" at the end of it.\nYou can then set a handler method named with the same label. The first parameter\npassed to the handler method is the result object, which contains "),s("code",[t._v("code")]),t._v(" (exit\ncode), "),s("code",[t._v("stdout")]),t._v(", "),s("code",[t._v("stdin")]),t._v(" and also "),s("code",[t._v("cmd")]),t._v(" -- a reference to the Fabula object\nrepresenting the parsed command. The second parameter is the "),s("strong",[t._v("Fabula")]),t._v(" context,\nwhich provides access to "),s("code",[t._v("settings")]),t._v(" and "),s("code",[t._v("abort()")]),t._v(".")])},function(){var t=this.$createElement,a=this._self._c||t;return a("p",[this._v("You can also provide a block of commands to run after the handler. This allows\nyou to add or change properties in "),a("strong",[this._v("Fabula")]),this._v("'s settings object prior to the\nJavaScript preprocessing.")])},function(){var t=this,a=t.$createElement,s=t._self._c||a;return s("div",{staticClass:"language-xml extra-class"},[s("pre",{pre:!0,attrs:{class:"language-xml"}},[s("code",[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("<")]),t._v("fabula")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(">")])]),t._v("\nexport default {\n fail: false,\n handle: (result) => {\n return {\n touchErrorCode: result.code\n }\n }\n}\n"),s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("")])]),t._v("\n\n"),s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("<")]),t._v("commands")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token attr-name"}},[t._v("local")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(">")])]),t._v("\ntouch /parent/doesnt/exist @handle:\n local write /tmp/fabula-handler-test:\n <%= touchErrorCode %>\ncat /tmp/fabula-handler-test\nrm /tmp/fabula-handler-test\n"),s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("")])]),t._v("\n")])])])},function(){var t=this,a=t.$createElement,s=t._self._c||a;return s("p",[t._v("The block after "),s("code",[t._v("@handle")]),t._v(" is only compiled after the current command has been\nexecuted and you've had a chance to "),s("strong",[t._v("handle")]),t._v(" it. The handling function (which\nmust match the "),s("code",[t._v("@name")]),t._v(" you use to tag the command) can return an object which\nis then merged back into "),s("strong",[t._v("Fabula")]),t._v(" settings object. The snippet above will\nresult in "),s("code",[t._v("1")]),t._v(" being written to the test file (which is then removed so no\ntesting files are left behind).")])},function(){var t=this.$createElement,a=this._self._c||t;return a("h2",{attrs:{id:"ensure"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#ensure","aria-hidden":"true"}},[this._v("#")]),this._v(" Ensure")])},function(){var t=this.$createElement,a=this._self._c||t;return a("p",[this._v("For the sole purpose of ensuring that a specific command runs successfully in\na "),a("strong",[this._v("Fabula")]),this._v(" task otherwise permissive to failure, you can also use the special\n"),a("code",[this._v("ensure")]),this._v(" command:")])},function(){var t=this,a=t.$createElement,s=t._self._c||a;return s("div",{staticClass:"language-xml extra-class"},[s("pre",{pre:!0,attrs:{class:"language-xml"}},[s("code",[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("<")]),t._v("commands")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(">")])]),t._v("\nunimportant command 1\nunimportant command 2\nensure yarn install\nyarn build\n"),s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("")])]),t._v("\n")])])])},function(){var t=this,a=t.$createElement,s=t._self._c||a;return s("div",{staticClass:"language-xml extra-class"},[s("pre",{pre:!0,attrs:{class:"language-xml"}},[s("code",[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("<")]),t._v("commands")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(">")])]),t._v("\nunimportant command 1\nunimportant command 2\nensure:\n yarn install\n yarn build\n"),s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("")])]),t._v("\n")])])])},function(){var t=this.$createElement,a=this._self._c||t;return a("h2",{attrs:{id:"retries"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#retries","aria-hidden":"true"}},[this._v("#")]),this._v(" Retries")])},function(){var t=this.$createElement,a=this._self._c||t;return a("p",[this._v("Fabula has a built-in retry mechanism. Use the "),a("code",[this._v("-r")]),this._v(" flag to specify the numbers\nof attempts that must be made for every command before accepting failure.")])},function(){var t=this.$createElement,a=this._self._c||t;return a("div",{staticClass:"language-sh extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[this._v("$ fabula all tasks/common-task -r 3\n")])])])}],!1,null,null,null);a.default=n.exports}}]); -------------------------------------------------------------------------------- /docs/assets/js/3.dd60ef97.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[3],{169:function(t,s,e){"use strict";e.r(s);var a=e(0),n=Object(a.a)({},function(){var t=this,s=t.$createElement,e=t._self._c||s;return e("div",{staticClass:"content"},[t._m(0),t._v(" "),t._m(1),t._v(" "),e("p",[t._v("At its core, "),e("strong",[t._v("Fabula")]),t._v(" is a simple Bash script preprocessor and runner. It lets\nyou run scripts "),e("strong",[t._v("locally")]),t._v(" and on "),e("strong",[t._v("remote servers")]),t._v(". "),e("strong",[t._v("Fabula")]),t._v(" (latin for\n"),e("em",[t._v("story")]),t._v(") is inspired by Python's "),e("a",{attrs:{href:"https://www.fabfile.org/",target:"_blank",rel:"noopener noreferrer"}},[t._v("Fabric"),e("OutboundLink")],1),t._v(".")]),t._v(" "),t._m(2),t._m(3),t._v(" "),t._m(4),t._m(5),t._v(" "),t._m(6),t._v(" "),t._m(7),t._m(8),t._v(" "),t._m(9),t._v(" "),t._m(10),t._v(" "),t._m(11),t._v(" "),t._m(12),t._v(" "),t._m(13),t._m(14),t._v(" "),t._m(15),t._m(16),t._v(" "),t._m(17),t._v(" "),e("p",[t._v("Fabula's compiler will respect Bash's semantics for most cases, but allows\nyou to embed interpolated JavaScript code ("),e("code",[t._v("<% %>")]),t._v(" and "),e("code",[t._v("<%= %>")]),t._v(") using\n"),e("a",{attrs:{href:"https://lodash.com/docs/4.17.11#template",target:"_blank",rel:"noopener noreferrer"}},[e("code",[t._v("lodash.template")]),e("OutboundLink")],1),t._v(" internally. Take for instance a "),e("code",[t._v("fabula.js")]),t._v("\nconfiguration file listing a series of files and contents:")]),t._v(" "),t._m(18),t._m(19),t._v(" "),t._m(20),t._m(21),t._v(" "),t._m(22),t._v(" "),t._m(23),t._v(" "),t._m(24),e("p",[t._v("See more about Fabula components "),e("router-link",{attrs:{to:"/components.html"}},[t._v("in its dedicated section")]),t._v(".")],1),t._v(" "),t._m(25),t._v(" "),e("p",[t._v("Please refer to "),e("a",{attrs:{href:"https://hire.jonasgalvez.com.br/2019/may/05/a-vuejs-inspired-task-runner",target:"_blank",rel:"noopener noreferrer"}},[t._v("this introductory blog post"),e("OutboundLink")],1),t._v(".")])])},[function(){var t=this.$createElement,s=this._self._c||t;return s("p",[s("img",{attrs:{src:"https://user-images.githubusercontent.com/12291/57234418-e9672600-6ff6-11e9-96bf-e8f2133efaa3.png",alt:"Fabula"}})])},function(){var t=this.$createElement,s=this._self._c||t;return s("h1",{attrs:{id:"introduction"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#introduction","aria-hidden":"true"}},[this._v("#")]),this._v(" Introduction")])},function(){var t=this.$createElement,s=this._self._c||t;return s("div",{staticClass:"language-sh extra-class"},[s("pre",{pre:!0,attrs:{class:"language-text"}},[s("code",[this._v('local echo "This runs on the local machine"\necho "This runs on the server"\n')])])])},function(){var t=this.$createElement,s=this._self._c||t;return s("p",[this._v("If you place the above snippet in a file named "),s("code",[this._v("echo.fab")]),this._v(" and configure a remote\nserver in Fabula's configuration file ("),s("code",[this._v("fabula.js")]),this._v("):")])},function(){var t=this,s=t.$createElement,e=t._self._c||s;return e("div",{staticClass:"language-js extra-class"},[e("pre",{pre:!0,attrs:{class:"language-js"}},[e("code",[e("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("export")]),t._v(" "),e("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("default")]),t._v(" "),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n ssh"),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n \tserver"),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n hostname"),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),e("span",{pre:!0,attrs:{class:"token string"}},[t._v("'1.2.3.4'")]),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n username"),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),e("span",{pre:!0,attrs:{class:"token string"}},[t._v("'user'")]),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n privateKey"),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),e("span",{pre:!0,attrs:{class:"token string"}},[t._v("'/path/to/key'")]),t._v("\n "),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n")])])])},function(){var t=this,s=t.$createElement,e=t._self._c||s;return e("p",[t._v("Executing "),e("code",[t._v("fabula server echo")]),t._v(" will run the script on "),e("code",[t._v("server")]),t._v(" (as specified\nunder "),e("code",[t._v("ssh")]),t._v(" in "),e("code",[t._v("fabula.js")]),t._v("), but every command preceded by "),e("code",[t._v("local")]),t._v(" will run\non the local machine.")])},function(){var t=this.$createElement,s=this._self._c||t;return s("p",[this._v("Conversely, if you omit the "),s("code",[this._v("server")]),this._v(" argument like below:")])},function(){var t=this.$createElement,s=this._self._c||t;return s("div",{staticClass:"language-sh extra-class"},[s("pre",{pre:!0,attrs:{class:"language-text"}},[s("code",[this._v("fabula echo\n")])])])},function(){var t=this.$createElement,s=this._self._c||t;return s("p",[this._v("It'll run the script strictly in local "),s("em",[this._v("mode")]),this._v(", in which case it will "),s("strong",[this._v("fail")]),this._v(" if\nit finds any command that is not preceded by "),s("code",[this._v("local")]),this._v(". The point is to allow both\ncontext-hybrid scripts and strictly local ones.")])},function(){var t=this.$createElement,s=this._self._c||t;return s("p",[this._v("To run on all available servers, use "),s("code",[this._v("fabula all ")]),this._v(".")])},function(){var t=this.$createElement,s=this._self._c||t;return s("h2",{attrs:{id:"context"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#context","aria-hidden":"true"}},[this._v("#")]),this._v(" Context")])},function(){var t=this.$createElement,s=this._self._c||t;return s("p",[this._v("If you have a "),s("strong",[this._v("Fabula")]),this._v(" task that is bound to run on multiple servers and\nparts of the commands rely on information specific to each server, you can\nreference the current server settings via "),s("code",[this._v("$server")]),this._v(":")])},function(){var t=this.$createElement,s=this._self._c||t;return s("p",[this._v("In "),s("code",[this._v("fabula.js")]),this._v(":")])},function(){var t=this,s=t.$createElement,e=t._self._c||s;return e("div",{staticClass:"language-js extra-class"},[e("pre",{pre:!0,attrs:{class:"language-js"}},[e("code",[e("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("export")]),t._v(" "),e("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("default")]),t._v(" "),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n ssh"),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n server1"),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n hostname"),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),e("span",{pre:!0,attrs:{class:"token string"}},[t._v("'1.2.3.4'")]),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n customSetting"),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),e("span",{pre:!0,attrs:{class:"token string"}},[t._v("'foo'")]),t._v("\n "),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n server2"),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n hostname"),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),e("span",{pre:!0,attrs:{class:"token string"}},[t._v("'1.2.3.4'")]),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n customSetting"),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),e("span",{pre:!0,attrs:{class:"token string"}},[t._v("'bar'")]),t._v("\n "),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n")])])])},function(){var t=this.$createElement,s=this._self._c||t;return s("p",[this._v("In "),s("code",[this._v("task.fab")]),this._v(":")])},function(){var t=this.$createElement,s=this._self._c||t;return s("div",{staticClass:"language-sh extra-class"},[s("pre",{pre:!0,attrs:{class:"language-text"}},[s("code",[this._v("echo <%= quote($server.customSetting) %>\n")])])])},function(){var t=this.$createElement,s=this._self._c||t;return s("p",[this._v("Running "),s("code",[this._v("fab all task")]),this._v(" will cause the correct command to run for each server.\nNote that "),s("code",[this._v("quote()")]),this._v(" is a special function that quotes strings for Bash, and\nis provided automatically by "),s("strong",[this._v("Fabula")]),this._v(".")])},function(){var t=this.$createElement,s=this._self._c||t;return s("h2",{attrs:{id:"preprocessor"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#preprocessor","aria-hidden":"true"}},[this._v("#")]),this._v(" Preprocessor")])},function(){var t=this,s=t.$createElement,e=t._self._c||s;return e("div",{staticClass:"language-js extra-class"},[e("pre",{pre:!0,attrs:{class:"language-js"}},[e("code",[e("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("export")]),t._v(" "),e("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("default")]),t._v(" "),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n files"),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n \tfile1"),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),e("span",{pre:!0,attrs:{class:"token string"}},[t._v("'Contents of file1'")]),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n \tfile2"),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),e("span",{pre:!0,attrs:{class:"token string"}},[t._v("'Contents of file2'")]),t._v("\n "),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n")])])])},function(){var t=this.$createElement,s=this._self._c||t;return s("p",[this._v("You could write a "),s("strong",[this._v("Fabula")]),this._v(" script as follows:")])},function(){var t=this.$createElement,s=this._self._c||t;return s("div",{staticClass:"language-sh extra-class"},[s("pre",{pre:!0,attrs:{class:"language-text"}},[s("code",[this._v("<% for (const file in files) { %>\nlocal echo <%= quote(files[file]) %> > <%= file %>\n<% } %>\n")])])])},function(){var t=this.$createElement,s=this._self._c||t;return s("p",[s("strong",[this._v("Fabula")]),this._v(" will first process all interpolated JavaScript and then run the resulting script.")])},function(){var t=this.$createElement,s=this._self._c||t;return s("h2",{attrs:{id:"components"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#components","aria-hidden":"true"}},[this._v("#")]),this._v(" Components")])},function(){var t=this.$createElement,s=this._self._c||t;return s("p",[this._v("Concentrating options in a single file ("),s("code",[this._v("fabula.js")]),this._v(") makes sense sometimes, but\nmight also create a mess if you have a lot of specific options pertaining to\none specific task. "),s("strong",[this._v("Fabula")]),this._v(" lets you combine settings and commands in a\n"),s("strong",[this._v("single-file component")]),this._v(", inspired by Vue. Here's what it looks like:")])},function(){var t=this,s=t.$createElement,e=t._self._c||s;return e("div",{staticClass:"language-xml extra-class"},[e("pre",{pre:!0,attrs:{class:"language-xml"}},[e("code",[e("span",{pre:!0,attrs:{class:"token tag"}},[e("span",{pre:!0,attrs:{class:"token tag"}},[e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("<")]),t._v("fabula")]),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(">")])]),t._v("\nexport default {\n files: {\n \tfile1: 'Contents of file1',\n \tfile2: 'Contents of file2'\n }\n}\n"),e("span",{pre:!0,attrs:{class:"token tag"}},[e("span",{pre:!0,attrs:{class:"token tag"}},[e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("")])]),t._v("\n\n"),e("span",{pre:!0,attrs:{class:"token tag"}},[e("span",{pre:!0,attrs:{class:"token tag"}},[e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("<")]),t._v("commands")]),e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(">")])]),t._v("\n<% for (const file in files) { %>\nlocal echo <%= quote(files[file]) %> > <%= file %>\n<% } %>\n"),e("span",{pre:!0,attrs:{class:"token tag"}},[e("span",{pre:!0,attrs:{class:"token tag"}},[e("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("")])]),t._v("\n")])])])},function(){var t=this.$createElement,s=this._self._c||t;return s("h2",{attrs:{id:"motivation"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#motivation","aria-hidden":"true"}},[this._v("#")]),this._v(" Motivation")])}],!1,null,null,null);s.default=n.exports}}]); -------------------------------------------------------------------------------- /docs/failure.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Failure | Fabula 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

Failure

By default, a single failing command will cause Fabula to exit and prevent 19 | any subsequent commands or tasks from running. You can disable this behaviour in Fabula's configuration file:

export default {
 20 |   fail: false
 21 | }
 22 | 

You can also set fail: false on a Fabula component.

Handlers

You can handle results for individual commands as well. A common example is 23 | handling a yarn install result. If fail is set to false, you may want 24 | to handle the result of certain commands.

<fabula>
 25 | export default {
 26 |   fail: false
 27 | }
 28 | </fabula>
 29 | 
 30 | <commands>
 31 | unimportant command 1
 32 | unimportant command 2
 33 | yarn install
 34 | yarn build
 35 | </commands>
 36 | 

In the above script, you would want to ensure yarn install finished 37 | succesfully before moving on to yarn build, even though you don't care about 38 | the first two unimportant commands.

Fabula lets you tag an individual command line and set a callback matching 39 | the tag given:

<fabula>
 40 | export default {
 41 |   fail: false,
 42 |   check({ code, stderr }, fabula) {
 43 |     if (code) {
 44 |       fabula.abort()
 45 |     }
 46 |   }
 47 | }
 48 | </fabula>
 49 | 
 50 | <commands>
 51 | unimportant command 1
 52 | unimportant command 2
 53 | yarn install @check
 54 | yarn build
 55 | </commands>
 56 | 

You can tag a command by placing a label prefixed with @ at the end of it. 57 | You can then set a handler method named with the same label. The first parameter 58 | passed to the handler method is the result object, which contains code (exit 59 | code), stdout, stdin and also cmd -- a reference to the Fabula object 60 | representing the parsed command. The second parameter is the Fabula context, 61 | which provides access to settings and abort().

You can also provide a block of commands to run after the handler. This allows 62 | you to add or change properties in Fabula's settings object prior to the 63 | JavaScript preprocessing.

Take this example from the test suite fixtures.

<fabula>
 64 | export default {
 65 |   fail: false,
 66 |   handle: (result) => {
 67 |     return {
 68 |       touchErrorCode: result.code
 69 |     }
 70 |   }
 71 | }
 72 | </fabula>
 73 | 
 74 | <commands local>
 75 | touch /parent/doesnt/exist @handle:
 76 |   local write /tmp/fabula-handler-test:
 77 |     <%= touchErrorCode %>
 78 | cat /tmp/fabula-handler-test
 79 | rm /tmp/fabula-handler-test
 80 | </commands>
 81 | 

The block after @handle is only compiled after the current command has been 82 | executed and you've had a chance to handle it. The handling function (which 83 | must match the @name you use to tag the command) can return an object which 84 | is then merged back into Fabula settings object. The snippet above will 85 | result in 1 being written to the test file (which is then removed so no 86 | testing files are left behind).

Ensure

For the sole purpose of ensuring that a specific command runs successfully in 87 | a Fabula task otherwise permissive to failure, you can also use the special 88 | ensure command:

<commands>
 89 | unimportant command 1
 90 | unimportant command 2
 91 | ensure yarn install
 92 | yarn build
 93 | </commands>
 94 | 

Or, alternatively, grouped in a block:

<commands>
 95 | unimportant command 1
 96 | unimportant command 2
 97 | ensure:
 98 |   yarn install
 99 |   yarn build
100 | </commands>
101 | 

Note that if you use <commands> with a prepend command all commands 102 | within ensure will be altered, but not ensure itself.

Retries

Fabula has a built-in retry mechanism. Use the -r flag to specify the numbers 103 | of attempts that must be made for every command before accepting failure.

$ fabula all tasks/common-task -r 3
104 | 
113 | 114 | 115 | 116 | --------------------------------------------------------------------------------