├── .github
├── dependabot.yml
├── release-drafter.yml
└── workflows
│ ├── ci.yml
│ ├── deploy.yml
│ └── push.yml
├── .gitignore
├── .kodiak.toml
├── .overcommit.yml
├── .prettierignore
├── .rubocop.yml
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── bin
├── console
├── mt
└── setup
├── custom_theme
├── breadcrumbs.html
└── main.html
├── docs
├── CNAME
├── api
│ ├── Host.md
│ ├── Logger.md
│ ├── Paths.md
│ ├── PluginDSL.md
│ ├── Remote.md
│ ├── Result.md
│ ├── TaskLibrary.md
│ └── testing
│ │ └── MockPluginTester.md
├── commands
│ ├── deploy.md
│ ├── init.md
│ ├── run.md
│ ├── setup.md
│ └── tasks.md
├── comparisons.md
├── configuration.md
├── css
│ └── extra.css
├── include
│ ├── common_options.md.include
│ ├── deploy_options.md.include
│ └── project_options.md.include
├── index.md
├── js
│ └── extra.js
├── plugins
│ ├── bundler.md
│ ├── core.md
│ ├── env.md
│ ├── git.md
│ ├── nodenv.md
│ ├── puma.md
│ ├── rails.md
│ └── rbenv.md
└── tutorials
│ ├── create-new-repo@2x.png
│ ├── deploying-rails-from-scratch.md
│ ├── publishing-a-plugin.md
│ ├── smallest-viable-droplet@2x.png
│ ├── ssh-auth@2x.png
│ ├── ubuntu-20-lts@2x.png
│ └── writing-custom-tasks.md
├── exe
└── tomo
├── lib
├── tomo.rb
└── tomo
│ ├── cli.rb
│ ├── cli
│ ├── command.rb
│ ├── common_options.rb
│ ├── completions.rb
│ ├── deploy_options.rb
│ ├── error.rb
│ ├── interrupted_error.rb
│ ├── options.rb
│ ├── parser.rb
│ ├── project_options.rb
│ ├── rules.rb
│ ├── rules
│ │ ├── argument.rb
│ │ ├── switch.rb
│ │ └── value_switch.rb
│ ├── rules_evaluator.rb
│ ├── state.rb
│ └── usage.rb
│ ├── colors.rb
│ ├── commands.rb
│ ├── commands
│ ├── completion_script.rb
│ ├── default.rb
│ ├── deploy.rb
│ ├── help.rb
│ ├── init.rb
│ ├── run.rb
│ ├── setup.rb
│ ├── tasks.rb
│ └── version.rb
│ ├── configuration.rb
│ ├── configuration
│ ├── dsl.rb
│ ├── dsl
│ │ ├── batch_block.rb
│ │ ├── config_file.rb
│ │ ├── environment_block.rb
│ │ ├── error_formatter.rb
│ │ ├── hosts_and_settings.rb
│ │ └── tasks_block.rb
│ ├── environment.rb
│ ├── glob.rb
│ ├── plugin_file_not_found_error.rb
│ ├── plugins_registry.rb
│ ├── plugins_registry
│ │ ├── file_resolver.rb
│ │ └── gem_resolver.rb
│ ├── project_not_found_error.rb
│ ├── role_based_task_filter.rb
│ ├── unknown_environment_error.rb
│ ├── unknown_plugin_error.rb
│ └── unspecified_environment_error.rb
│ ├── console.rb
│ ├── console
│ ├── key_reader.rb
│ ├── menu.rb
│ └── non_interactive_error.rb
│ ├── error.rb
│ ├── error
│ └── suggestions.rb
│ ├── host.rb
│ ├── logger.rb
│ ├── logger
│ └── tagged_io.rb
│ ├── path.rb
│ ├── paths.rb
│ ├── plugin.rb
│ ├── plugin
│ ├── bundler.rb
│ ├── bundler
│ │ ├── helpers.rb
│ │ └── tasks.rb
│ ├── core.rb
│ ├── core
│ │ ├── helpers.rb
│ │ └── tasks.rb
│ ├── env.rb
│ ├── env
│ │ └── tasks.rb
│ ├── git.rb
│ ├── git
│ │ ├── helpers.rb
│ │ └── tasks.rb
│ ├── nodenv.rb
│ ├── nodenv
│ │ └── tasks.rb
│ ├── puma.rb
│ ├── puma
│ │ ├── systemd
│ │ │ ├── service.erb
│ │ │ └── socket.erb
│ │ └── tasks.rb
│ ├── rails.rb
│ ├── rails
│ │ ├── helpers.rb
│ │ └── tasks.rb
│ ├── rbenv.rb
│ ├── rbenv
│ │ └── tasks.rb
│ └── testing.rb
│ ├── plugin_dsl.rb
│ ├── remote.rb
│ ├── result.rb
│ ├── runtime.rb
│ ├── runtime
│ ├── concurrent_ruby_load_error.rb
│ ├── concurrent_ruby_thread_pool.rb
│ ├── context.rb
│ ├── current.rb
│ ├── execution_plan.rb
│ ├── explanation.rb
│ ├── host_execution_step.rb
│ ├── inline_thread_pool.rb
│ ├── no_tasks_error.rb
│ ├── privileged_task.rb
│ ├── settings_interpolation.rb
│ ├── settings_required_error.rb
│ ├── task_aborted_error.rb
│ ├── task_runner.rb
│ ├── template_not_found_error.rb
│ └── unknown_task_error.rb
│ ├── script.rb
│ ├── shell_builder.rb
│ ├── ssh.rb
│ ├── ssh
│ ├── child_process.rb
│ ├── connection.rb
│ ├── connection_error.rb
│ ├── connection_validator.rb
│ ├── error.rb
│ ├── executable_error.rb
│ ├── options.rb
│ ├── permission_error.rb
│ ├── script_error.rb
│ ├── unknown_error.rb
│ └── unsupported_version_error.rb
│ ├── task_api.rb
│ ├── task_library.rb
│ ├── templates
│ ├── config.rb.erb
│ └── plugin.rb.erb
│ ├── testing.rb
│ ├── testing
│ ├── Dockerfile
│ ├── cli_extensions.rb
│ ├── cli_tester.rb
│ ├── connection.rb
│ ├── docker_image.rb
│ ├── host_extensions.rb
│ ├── local.rb
│ ├── log_capturing.rb
│ ├── mock_plugin_tester.rb
│ ├── mocked_exec_error.rb
│ ├── mocked_exit_error.rb
│ ├── remote_extensions.rb
│ ├── ssh_extensions.rb
│ ├── systemctl.rb
│ ├── tomo_test_ed25519
│ ├── tomo_test_ed25519.pub
│ └── ubuntu_setup.sh
│ └── version.rb
├── mkdocs.yml
├── readme_images
├── README.md
├── console.css
├── console.js
├── record_console.rb
├── tomo-deploy-dry-run.png
├── tomo-deploy-help.png
├── tomo-help.png
├── tomo-init.png
├── tomo-run-hello-dry-run.png
├── tomo-run-hello.png
├── tomo-run-rails-console-dry-run.png
├── tomo-run-rails-console.png
├── tomo-setup-dry-run.png
├── tomo-setup-help.png
└── tomo-tasks.png
├── requirements.txt
├── test
├── e2e
│ └── rails_setup_deploy_e2e_test.rb
├── fixtures
│ └── template.erb
├── test_helper.rb
└── tomo
│ ├── cli
│ └── completions_test.rb
│ ├── cli_test.rb
│ ├── colors_test.rb
│ ├── commands
│ └── init_test.rb
│ ├── configuration_test.rb
│ ├── console
│ └── key_reader_test.rb
│ ├── console_test.rb
│ ├── host_test.rb
│ ├── paths_test.rb
│ ├── plugin
│ ├── bundler
│ │ └── tasks_test.rb
│ ├── core
│ │ ├── helpers_test.rb
│ │ └── tasks_test.rb
│ ├── env
│ │ └── tasks_test.rb
│ ├── git
│ │ └── tasks_test.rb
│ ├── nodenv
│ │ └── tasks_test.rb
│ ├── puma
│ │ └── tasks_test.rb
│ ├── rails
│ │ ├── helpers_test.rb
│ │ └── tasks_test.rb
│ └── rbenv
│ │ └── tasks_test.rb
│ ├── runtime
│ ├── execution_plan_test.rb
│ └── settings_interpolation_test.rb
│ ├── runtime_test.rb
│ ├── shell_builder_test.rb
│ └── task_api_test.rb
└── tomo.gemspec
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: bundler
4 | directory: "/"
5 | schedule:
6 | interval: monthly
7 | time: "16:00"
8 | timezone: America/Los_Angeles
9 | open-pull-requests-limit: 10
10 | labels:
11 | - "🏠 Housekeeping"
12 | - package-ecosystem: github-actions
13 | directory: "/"
14 | schedule:
15 | interval: monthly
16 | time: "16:00"
17 | timezone: America/Los_Angeles
18 | open-pull-requests-limit: 10
19 | labels:
20 | - "🏠 Housekeeping"
21 | - package-ecosystem: pip
22 | directory: "/"
23 | schedule:
24 | interval: monthly
25 | time: "16:00"
26 | timezone: America/Los_Angeles
27 | open-pull-requests-limit: 10
28 | labels:
29 | - "🏠 Housekeeping"
30 |
--------------------------------------------------------------------------------
/.github/release-drafter.yml:
--------------------------------------------------------------------------------
1 | name-template: "$RESOLVED_VERSION"
2 | tag-template: "v$RESOLVED_VERSION"
3 | categories:
4 | - title: "⚠️ Breaking Changes"
5 | label: "⚠️ Breaking"
6 | - title: "✨ New Features"
7 | label: "✨ Feature"
8 | - title: "🐛 Bug Fixes"
9 | label: "🐛 Bug Fix"
10 | - title: "📚 Documentation"
11 | label: "📚 Docs"
12 | - title: "🏠 Housekeeping"
13 | label: "🏠 Housekeeping"
14 | version-resolver:
15 | minor:
16 | labels:
17 | - "⚠️ Breaking"
18 | - "✨ Feature"
19 | default: patch
20 | change-template: "- $TITLE (#$NUMBER) @$AUTHOR"
21 | no-changes-template: "- No changes"
22 | template: |
23 | $CHANGES
24 |
25 | **Full Changelog:** https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION
26 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | pull_request:
4 | push:
5 | branches:
6 | - main
7 | jobs:
8 | build_docs:
9 | name: "Build Docs"
10 | runs-on: ubuntu-latest
11 | if: ${{ github.ref != 'refs/heads/main' }}
12 | steps:
13 | - uses: actions/checkout@v4
14 | - uses: actions/setup-python@v5
15 | with:
16 | python-version: "3.10"
17 | cache: "pip"
18 | - run: pip install -r requirements.txt
19 | - run: mkdocs build
20 | rubocop:
21 | name: "Rubocop"
22 | runs-on: ubuntu-latest
23 | steps:
24 | - uses: actions/checkout@v4
25 | - uses: ruby/setup-ruby@v1
26 | with:
27 | ruby-version: "ruby"
28 | bundler-cache: true
29 | - run: bundle exec rubocop
30 | test:
31 | name: "Test / Ruby ${{ matrix.ruby }}"
32 | runs-on: ubuntu-latest
33 | strategy:
34 | matrix:
35 | ruby: ["3.1", "3.2", "3.3", "3.4", "head"]
36 | steps:
37 | - uses: actions/checkout@v4
38 | - uses: ruby/setup-ruby@v1
39 | with:
40 | ruby-version: ${{ matrix.ruby }}
41 | bundler-cache: true
42 | - run: git config --global user.name 'github-actions[bot]'
43 | - run: git config --global user.email 'github-actions[bot]@users.noreply.github.com'
44 | - run: bundle exec rake test
45 | test_rails_deploy:
46 | name: "Test / Rails Deploy"
47 | runs-on: ubuntu-latest
48 | needs: [rubocop, test]
49 | steps:
50 | - uses: actions/checkout@v4
51 | - uses: ruby/setup-ruby@v1
52 | with:
53 | ruby-version: "ruby"
54 | bundler-cache: true
55 | - run: bundle exec rake test:e2e
56 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Deploy
2 | on:
3 | push:
4 | branches:
5 | - main
6 | jobs:
7 | docs:
8 | name: "Docs"
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 | with:
13 | fetch-depth: 0
14 | - uses: actions/setup-python@v5
15 | with:
16 | python-version: "3.10"
17 | cache: "pip"
18 | - run: pip install -r requirements.txt
19 | - run: cat README.md >> docs/index.md
20 | - run: cp -R readme_images docs/
21 | - run: git config --local user.name 'github-actions[bot]'
22 | - run: git config --local user.email 'github-actions[bot]@users.noreply.github.com'
23 | - run: mkdocs gh-deploy -m 'Deployed {sha} with mkdocs {version} [ci skip]'
24 |
--------------------------------------------------------------------------------
/.github/workflows/push.yml:
--------------------------------------------------------------------------------
1 | name: Release Drafter
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | update_release_draft:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: release-drafter/release-drafter@v6
13 | env:
14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.tomo/
3 | /.yardoc
4 | /_yardoc/
5 | /coverage/
6 | /doc/
7 | /pkg/
8 | /site/
9 | /spec/reports/
10 | /tmp/
11 | /Gemfile.lock
12 | .python-version
13 |
--------------------------------------------------------------------------------
/.kodiak.toml:
--------------------------------------------------------------------------------
1 | # .kodiak.toml
2 | # Minimal config. version is the only required field.
3 | version = 1
4 |
5 | [merge.automerge_dependencies]
6 | # auto merge all PRs opened by "dependabot" that are "minor" or "patch" version upgrades. "major" version upgrades will be ignored.
7 | versions = ["minor", "patch"]
8 | usernames = ["dependabot"]
9 |
10 | # if using `update.always`, add dependabot to `update.ignore_usernames` to allow
11 | # dependabot to update and close stale dependency upgrades.
12 | [update]
13 | ignored_usernames = ["dependabot"]
14 |
--------------------------------------------------------------------------------
/.overcommit.yml:
--------------------------------------------------------------------------------
1 | # Overcommit hooks run automatically on certain git operations, like "git commit".
2 | # For a complete list of options that you can use to customize hooks, see:
3 | # https://github.com/sds/overcommit
4 |
5 | gemfile: false
6 | verify_signatures: false
7 |
8 | PreCommit:
9 | BundleCheck:
10 | enabled: true
11 |
12 | FixMe:
13 | enabled: true
14 | keywords: ["FIXME"]
15 | exclude:
16 | - .overcommit.yml
17 | - lib/tomo/templates/config.rb.erb
18 |
19 | LocalPathsInGemfile:
20 | enabled: true
21 |
22 | RuboCop:
23 | enabled: true
24 | required_executable: bundle
25 | command: ["bundle", "exec", "rubocop"]
26 | on_warn: fail
27 |
28 | YamlSyntax:
29 | enabled: true
30 |
31 | PostCheckout:
32 | ALL:
33 | quiet: true
34 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | /CODE_OF_CONDUCT.md
2 | /custom_theme/*.html
3 | /docs/commands/*.md
4 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | plugins:
2 | - rubocop-minitest
3 | - rubocop-packaging
4 | - rubocop-performance
5 | - rubocop-rake
6 | - rubocop-md
7 |
8 | AllCops:
9 | DisplayCopNames: true
10 | DisplayStyleGuide: true
11 | NewCops: enable
12 | TargetRubyVersion: 3.1
13 | Exclude:
14 | - "bin/**/*"
15 | - "lib/tomo/templates/config.rb"
16 | - "readme_images/**/*"
17 | - "tmp/**/*"
18 | - "vendor/**/*"
19 |
20 | Layout/FirstArrayElementIndentation:
21 | EnforcedStyle: consistent
22 |
23 | Layout/FirstArrayElementLineBreak:
24 | Enabled: true
25 |
26 | Layout/FirstHashElementLineBreak:
27 | Enabled: true
28 |
29 | Layout/FirstMethodArgumentLineBreak:
30 | Enabled: true
31 |
32 | Layout/HashAlignment:
33 | EnforcedColonStyle:
34 | - table
35 | - key
36 | EnforcedHashRocketStyle:
37 | - table
38 | - key
39 |
40 | Layout/MultilineArrayLineBreaks:
41 | Enabled: true
42 |
43 | Layout/MultilineHashKeyLineBreaks:
44 | Enabled: true
45 |
46 | Layout/MultilineMethodArgumentLineBreaks:
47 | Enabled: true
48 |
49 | Layout/MultilineMethodCallIndentation:
50 | EnforcedStyle: indented
51 |
52 | Layout/SpaceAroundEqualsInParameterDefault:
53 | EnforcedStyle: no_space
54 |
55 | Metrics/AbcSize:
56 | Max: 20
57 | Exclude:
58 | - "test/**/*"
59 |
60 | Metrics/BlockLength:
61 | Exclude:
62 | - "*.gemspec"
63 | - "Rakefile"
64 |
65 | Metrics/ClassLength:
66 | Exclude:
67 | - "test/**/*"
68 |
69 | Metrics/MethodLength:
70 | Max: 12
71 | CountAsOne: ["heredoc"]
72 | Exclude:
73 | - "test/**/*"
74 |
75 | Metrics/ParameterLists:
76 | Max: 6
77 |
78 | Minitest/EmptyLineBeforeAssertionMethods:
79 | Enabled: false
80 |
81 | Minitest/MultipleAssertions:
82 | Max: 5
83 |
84 | Minitest/TestFileName:
85 | Exclude:
86 | - "**/*.md"
87 |
88 | Naming/MemoizedInstanceVariableName:
89 | Enabled: false
90 |
91 | Naming/VariableNumber:
92 | Enabled: false
93 |
94 | Rake/Desc:
95 | Enabled: false
96 |
97 | Style/BarePercentLiterals:
98 | EnforcedStyle: percent_q
99 |
100 | Style/ClassAndModuleChildren:
101 | Enabled: false
102 |
103 | Style/Documentation:
104 | Enabled: false
105 |
106 | Style/DoubleNegation:
107 | Enabled: false
108 |
109 | Style/EmptyMethod:
110 | Enabled: false
111 |
112 | Style/FetchEnvVar:
113 | Enabled: false
114 |
115 | Style/FormatStringToken:
116 | Enabled: false
117 |
118 | Style/NumericLiterals:
119 | Enabled: false
120 |
121 | Style/NumericPredicate:
122 | Enabled: false
123 |
124 | Style/StringConcatenation:
125 | Enabled: false
126 |
127 | Style/StringLiterals:
128 | EnforcedStyle: double_quotes
129 |
130 | Style/TrivialAccessors:
131 | AllowPredicates: true
132 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Release notes for this project are kept here: https://github.com/mattbrictson/tomo/releases
2 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contribution guide
2 |
3 | Tomo wouldn't be possible without the generosity of the open source community. Thank you for your input and support! Here are some guidelines to follow when contributing.
4 |
5 | ## 🐛 Bug reports
6 |
7 | - Explain the troubleshooting steps you've already tried
8 | - Use `--debug` and `--trace` to provide additional detail
9 | - Use GitHub-flavored Markdown, especially code fences ```
to format logs
10 | - Redact any sensitive information
11 | - Include reproduction steps or code for a failing test case if you can
12 |
13 | ## ✨ Feature requests
14 |
15 | Ideas for new tomo features are appreciated!
16 |
17 | > The current guiding principle for tomo is to be a "batteries included" tool that encapsulates best practices for deploying Rails apps to inexpensive cloud hosting infrastructure. In considering feature requests, weight will be given to keeping tomo simple to use and simple to maintain, even if that means that certain edge cases and deployment scenarios are unsupported.
18 |
19 | - Show examples of how the feature would work
20 | - Explain your motivation for requesting the feature
21 | - Would it be useful for the majority of tomo users?
22 | - Is it a breaking change?
23 |
24 | ## ⤴️ Pull requests
25 |
26 | > Protip: If you have a big change in mind, it is a good idea to open an issue first to propose the idea and get some initial feedback.
27 |
28 | ### Working on code
29 |
30 | - Run `bundle install` to install dependencies
31 | - `bin/console` opens an irb console if you need a REPL to try things out
32 | - `bundle exec tomo` will run your working copy of tomo
33 | - `rake install` will install your working copy of tomo globally (so you can test it in other projects)
34 | - Make sure to run `rake` to run all tests and RuboCop checks prior to opening a PR
35 |
36 | ### Working on docs
37 |
38 | - You will need python to build the docs
39 | - Run `pip install -r requirements.txt` to install the relevant python packages
40 | - Start the docs server by running `mkdocs serve`
41 | - Browse the rendered docs at
", "#{ascii}") 45 | 46 | out = File.join(Dir.tmpdir, "console-#{SecureRandom.hex(8)}.html") 47 | IO.write(out, html) 48 | %w[console.css console.js].each do |file| 49 | FileUtils.cp(File.expand_path(file, __dir__), Dir.tmpdir) 50 | end 51 | system "open", out 52 | -------------------------------------------------------------------------------- /readme_images/tomo-deploy-dry-run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/tomo/15d7faccc8c1b91750196284e2e0fa98fd0c1116/readme_images/tomo-deploy-dry-run.png -------------------------------------------------------------------------------- /readme_images/tomo-deploy-help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/tomo/15d7faccc8c1b91750196284e2e0fa98fd0c1116/readme_images/tomo-deploy-help.png -------------------------------------------------------------------------------- /readme_images/tomo-help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/tomo/15d7faccc8c1b91750196284e2e0fa98fd0c1116/readme_images/tomo-help.png -------------------------------------------------------------------------------- /readme_images/tomo-init.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/tomo/15d7faccc8c1b91750196284e2e0fa98fd0c1116/readme_images/tomo-init.png -------------------------------------------------------------------------------- /readme_images/tomo-run-hello-dry-run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/tomo/15d7faccc8c1b91750196284e2e0fa98fd0c1116/readme_images/tomo-run-hello-dry-run.png -------------------------------------------------------------------------------- /readme_images/tomo-run-hello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/tomo/15d7faccc8c1b91750196284e2e0fa98fd0c1116/readme_images/tomo-run-hello.png -------------------------------------------------------------------------------- /readme_images/tomo-run-rails-console-dry-run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/tomo/15d7faccc8c1b91750196284e2e0fa98fd0c1116/readme_images/tomo-run-rails-console-dry-run.png -------------------------------------------------------------------------------- /readme_images/tomo-run-rails-console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/tomo/15d7faccc8c1b91750196284e2e0fa98fd0c1116/readme_images/tomo-run-rails-console.png -------------------------------------------------------------------------------- /readme_images/tomo-setup-dry-run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/tomo/15d7faccc8c1b91750196284e2e0fa98fd0c1116/readme_images/tomo-setup-dry-run.png -------------------------------------------------------------------------------- /readme_images/tomo-setup-help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/tomo/15d7faccc8c1b91750196284e2e0fa98fd0c1116/readme_images/tomo-setup-help.png -------------------------------------------------------------------------------- /readme_images/tomo-tasks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/tomo/15d7faccc8c1b91750196284e2e0fa98fd0c1116/readme_images/tomo-tasks.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | markdown == 3.8 2 | markdown_include == 0.8.1 3 | mdx_truly_sane_lists == 1.3 4 | mkdocs == 1.6.1 5 | smarty == 0.3.3 6 | jinja2==3.1.6 7 | -------------------------------------------------------------------------------- /test/e2e/rails_setup_deploy_e2e_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | require "net/http" 6 | require "securerandom" 7 | 8 | class RailsSetupDeployE2ETest < Minitest::Test 9 | include Tomo::Testing::Local 10 | 11 | def setup 12 | @docker = Tomo::Testing::DockerImage.new 13 | @docker.build_and_run 14 | end 15 | 16 | def teardown 17 | @docker.stop 18 | end 19 | 20 | def test_rails_setup_deploy 21 | in_cloned_rails_repo do 22 | bundle_exec("tomo init") 23 | config = File.read(".tomo/config.rb") 24 | config.sub!( 25 | /host ".*"/, 26 | %Q(host "#{@docker.host.user}@#{@docker.host.address}", port: #{@docker.host.port}) 27 | ) 28 | config.sub!( 29 | /set rbenv_ruby_version:\s*\S+/, 30 | "set rbenv_ruby_version: #{File.read('.ruby-version').strip.inspect}" 31 | ) 32 | config << <<~CONFIG 33 | set(#{@docker.ssh_settings.inspect}) 34 | CONFIG 35 | File.write(".tomo/config.rb", config) 36 | 37 | bundle_exec("tomo run env:set DATABASE_URL=sqlite3:/var/www/rails-new/shared/production.sqlite3") 38 | bundle_exec("tomo setup") 39 | bundle_exec("tomo deploy") 40 | 41 | # Pause to allow puma to completely finish booting 42 | sleep 5 43 | 44 | rails_uri = URI("http://localhost:#{@docker.puma_port}/") 45 | rails_http_response = Net::HTTP.get_response(rails_uri) 46 | 47 | assert_kind_of(Net::HTTPSuccess, rails_http_response) 48 | assert_match(/It works!/i, rails_http_response.body) 49 | end 50 | end 51 | 52 | private 53 | 54 | def bundle_exec(command) 55 | with_tomo_gemfile do 56 | full_cmd = "bundle exec #{command}" 57 | puts ">>> #{full_cmd}" 58 | system(full_cmd, exception: true) 59 | end 60 | end 61 | 62 | def in_cloned_rails_repo(&block) 63 | in_temp_dir do 64 | repo = "https://github.com/mattbrictson/rails-new.git" 65 | capture("git clone #{repo}") 66 | Dir.chdir("rails-new", &block) 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /test/fixtures/template.erb: -------------------------------------------------------------------------------- 1 | Hello, <%= settings[:application] %>! 2 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift File.expand_path("../lib", __dir__) 4 | require "tomo/testing" 5 | 6 | require "minitest/autorun" 7 | -------------------------------------------------------------------------------- /test/tomo/cli/completions_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class Tomo::CLI::CompletionsTest < Minitest::Test 6 | def setup 7 | @tester = Tomo::Testing::CLITester.new 8 | end 9 | 10 | def test_completions_include_setting_names 11 | @tester.run "init" 12 | @tester.run "--complete", "deploy", "-s" 13 | 14 | assert_match(/^git_branch=$/, @tester.stdout) 15 | assert_match(/^git_url=$/, @tester.stdout) 16 | end 17 | 18 | def test_completes_task_name_even_without_run_command 19 | @tester.run "init" 20 | @tester.run "--complete-word", "rails:" 21 | 22 | assert_match(/^console $/, @tester.stdout) 23 | assert_match(/^db_migrate $/, @tester.stdout) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/tomo/cli_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class Tomo::CLITest < Minitest::Test 6 | def setup 7 | @tester = Tomo::Testing::CLITester.new 8 | end 9 | 10 | def test_execute_task_with_implicit_run_command 11 | @tester.run "init" 12 | @tester.run "bundler:install", "--dry-run" 13 | assert_match "Simulated bundler:install", @tester.stdout 14 | end 15 | 16 | def test_commands_can_be_abbreviated 17 | stdout, = capture_io { @tester.run "i", "--help" } 18 | assert_match "Usage: tomo init", stdout 19 | 20 | stdout, = capture_io { @tester.run "d", "--help" } 21 | assert_match "Usage: tomo deploy", stdout 22 | end 23 | 24 | def test_dash_t_is_alias_for_tasks 25 | @tester.run "init" 26 | @tester.run "-T" 27 | assert_match "core:clean_releases", @tester.stdout 28 | assert_match "core:setup_directories", @tester.stdout 29 | end 30 | 31 | def test_suggests_installing_missing_plugin 32 | @tester.run "init" 33 | @tester.run "foo:setup", raise_on_error: false 34 | assert_match(/did you forget to install the foo plugin/i, @tester.stderr) 35 | end 36 | 37 | def test_prints_error_when_config_has_syntax_error 38 | @tester.in_temp_dir do 39 | FileUtils.mkdir_p(".tomo") 40 | File.write(".tomo/config.rb", <<~CONFIG) 41 | plugin "git" 42 | deploy do 43 | run "git:clone 44 | run "git:create_release" 45 | end 46 | CONFIG 47 | end 48 | @tester.run "deploy", raise_on_error: false 49 | assert_match(<<~OUTPUT.strip, @tester.stderr.gsub(/^ /, "")) 50 | ERROR: Configuration syntax error in .tomo/config.rb at line 4. 51 | 52 | 3: run "git:clone 53 | → 4: run "git:create_release" 54 | 5: end 55 | 56 | SyntaxError: .tomo/config.rb:4: syntax error 57 | OUTPUT 58 | end 59 | 60 | def test_prints_error_when_config_dsl_is_used_incorrectly 61 | @tester.in_temp_dir do 62 | FileUtils.mkdir_p(".tomo") 63 | File.write(".tomo/config.rb", <<~CONFIG) 64 | plugin "git" 65 | deploy do 66 | run 67 | run "git:create_release" 68 | end 69 | CONFIG 70 | end 71 | @tester.run "deploy", raise_on_error: false 72 | assert_equal(<<~OUTPUT, @tester.stderr.gsub(/^ /, "")) 73 | 74 | ERROR: Configuration syntax error in .tomo/config.rb at line 3. 75 | 76 | 2: deploy do 77 | → 3: run 78 | 4: run "git:create_release" 79 | 80 | ArgumentError: wrong number of arguments (given 0, expected 1) 81 | 82 | Visit https://tomo.mattbrictson.com/configuration for syntax reference. 83 | You can run this command again with --trace for a full backtrace. 84 | 85 | OUTPUT 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /test/tomo/colors_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class Tomo::ColorsTest < Minitest::Test 6 | def setup 7 | # This forces color support detection to happen again 8 | Tomo::Colors.remove_instance_variable(:@enabled) 9 | end 10 | 11 | def teardown 12 | Tomo::Colors.enabled = false 13 | end 14 | 15 | def test_enabled_by_default_if_tty 16 | with_tty(true) do 17 | with_env({}) do 18 | assert_predicate(Tomo::Colors, :enabled?) 19 | end 20 | end 21 | end 22 | 23 | def test_disabled_by_default_if_not_tty 24 | with_tty(false) do 25 | with_env({}) do 26 | refute_predicate(Tomo::Colors, :enabled?) 27 | end 28 | end 29 | end 30 | 31 | def test_enabled_by_clicolor_force 32 | with_tty(false) do 33 | with_env("CLICOLOR_FORCE" => "1") do 34 | assert_predicate(Tomo::Colors, :enabled?) 35 | end 36 | end 37 | end 38 | 39 | def test_disabled_by_no_color 40 | with_tty(true) do 41 | with_env("NO_COLOR" => "1") do 42 | refute_predicate(Tomo::Colors, :enabled?) 43 | end 44 | end 45 | end 46 | 47 | def test_disabled_by_dumb_term 48 | with_tty(true) do 49 | with_env("TERM" => "dumb") do 50 | refute_predicate(Tomo::Colors, :enabled?) 51 | end 52 | end 53 | end 54 | 55 | private 56 | 57 | def with_tty(tty, &block) 58 | $stdout.stub(:tty?, tty) { $stderr.stub(:tty?, tty, &block) } 59 | end 60 | 61 | def with_env(env, &) 62 | ENV.stub(:[], ->(name) { env[name] }, &) 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/tomo/configuration_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class Tomo::ConfigurationTest < Minitest::Test 6 | include Tomo::Testing::Local 7 | 8 | def test_parses_a_config_file_that_contains_frozen_string_literals 9 | in_temp_dir do 10 | FileUtils.mkdir ".tomo" 11 | File.write(".tomo/config.rb", <<~CONFIG) 12 | # frozen_string_literal: true 13 | 14 | setup do 15 | run "nginx:setup", privileged: true 16 | end 17 | CONFIG 18 | 19 | parsed = Tomo::Configuration.from_config_rb 20 | 21 | assert_instance_of(Tomo::Configuration, parsed) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/tomo/console/key_reader_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "stringio" 5 | 6 | class Tomo::Console 7 | class KeyReaderTest < Minitest::Test 8 | def setup 9 | @input = StringIO.new 10 | @input.define_singleton_method(:raw) do |&block| 11 | block.call 12 | end 13 | end 14 | 15 | def test_reads_a_single_keystroke 16 | @input << "h" 17 | @input.rewind 18 | 19 | key_reader = KeyReader.new(@input) 20 | char = key_reader.next 21 | 22 | assert_equal("h", char) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/tomo/console_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "stringio" 5 | 6 | class Tomo::ConsoleTest < Minitest::Test 7 | def test_interactive_is_true_for_tty 8 | assert_predicate Tomo::Console.new({}, tty), :interactive? 9 | end 10 | 11 | def test_interactive_is_false_for_ci_env 12 | refute_predicate Tomo::Console.new({ "CIRCLECI" => "1" }, tty), :interactive? 13 | end 14 | 15 | def test_interactive_is_false_non_tty 16 | refute_predicate Tomo::Console.new({}, non_tty), :interactive? 17 | end 18 | 19 | def test_prompt_answer_does_not_contain_newline 20 | stdout = StringIO.new 21 | console = Tomo::Console.new({}, tty("yes\n"), stdout) 22 | answer = console.prompt("Are you sure? ") 23 | assert_equal("Are you sure? ", stdout.string) 24 | assert_equal("yes", answer) 25 | end 26 | 27 | def test_prompt_raises_if_not_tty 28 | console = Tomo::Console.new({}, non_tty("yes\n")) 29 | error = assert_raises(Tomo::Console::NonInteractiveError) { console.prompt("Are you sure? ") } 30 | assert_match(/requires an interactive console/i, error.to_console) 31 | end 32 | 33 | def test_prompt_raises_if_ci 34 | console = Tomo::Console.new({ "CIRCLECI" => "1" }, tty("yes\n")) 35 | error = assert_raises(Tomo::Console::NonInteractiveError) { console.prompt("Are you sure? ") } 36 | assert_match(/appears to be a non-interactive CI environment/i, error.to_console) 37 | end 38 | 39 | def test_menu_raises_if_not_tty 40 | console = Tomo::Console.new({}, non_tty("yes\n")) 41 | error = assert_raises(Tomo::Console::NonInteractiveError) { console.menu("Are you sure? ", choices: %w[y n]) } 42 | assert_match(/requires an interactive console/i, error.to_console) 43 | end 44 | 45 | def test_menu_raises_if_ci 46 | console = Tomo::Console.new({ "CIRCLECI" => "1" }, tty("yes\n")) 47 | error = assert_raises(Tomo::Console::NonInteractiveError) { console.menu("Are you sure? ", choices: %w[y n]) } 48 | assert_match(/appears to be a non-interactive CI environment/i, error.to_console) 49 | end 50 | 51 | private 52 | 53 | def tty(data="") 54 | StringIO.new(data).tap do |io| 55 | def io.raw 56 | end 57 | 58 | def io.tty? 59 | true 60 | end 61 | end 62 | end 63 | 64 | def non_tty(data="") 65 | StringIO.new(data) 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /test/tomo/host_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class Tomo::HostTest < Minitest::Test 6 | def test_parse_hostname 7 | host = Tomo::Host.parse("app.example.com") 8 | assert_equal("app.example.com", host.address) 9 | assert_equal(22, host.port) 10 | assert_empty(host.roles) 11 | assert_nil(host.log_prefix) 12 | assert_nil(host.user) 13 | end 14 | 15 | def test_parse_hostname_with_user 16 | host = Tomo::Host.parse("deployer@app.example.com") 17 | assert_equal("app.example.com", host.address) 18 | assert_equal(22, host.port) 19 | assert_equal("deployer", host.user) 20 | assert_empty(host.roles) 21 | assert_nil(host.log_prefix) 22 | end 23 | 24 | def test_parse_ip_address_with_user 25 | host = Tomo::Host.parse("my.user@10.1.19.2") 26 | assert_equal("10.1.19.2", host.address) 27 | assert_equal(22, host.port) 28 | assert_equal("my.user", host.user) 29 | assert_empty(host.roles) 30 | assert_nil(host.log_prefix) 31 | end 32 | 33 | def test_parse_with_options 34 | host = Tomo::Host.parse("deployer@app.example.com", port: 8022, log_prefix: "one", roles: %w[db web]) 35 | assert_equal("app.example.com", host.address) 36 | assert_equal(8022, host.port) 37 | assert_equal("deployer", host.user) 38 | assert_equal("one", host.log_prefix) 39 | assert_equal(%w[db web], host.roles) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/tomo/paths_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class Tomo::PathsTest < Minitest::Test 6 | def test_raises_if_setting_does_not_exist 7 | paths = Tomo::Paths.new({}) 8 | assert_raises(NoMethodError) { paths.storage } 9 | end 10 | 11 | def test_returns_nil_if_setting_is_nil 12 | paths = Tomo::Paths.new(storage_path: nil) 13 | assert_nil(paths.storage) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/tomo/plugin/git/tasks_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "tomo/plugin/git" 5 | 6 | class Tomo::Plugin::Git::TasksTest < Minitest::Test 7 | def test_config_sets_name_and_email_with_user_by_default 8 | tester = configure 9 | tester.run_task("git:config") 10 | assert_equal( 11 | [ 12 | "git config --global user.name testing", 13 | "git config --global user.email testing@example.com" 14 | ], 15 | tester.executed_scripts 16 | ) 17 | end 18 | 19 | def test_config_sets_name_and_email_based_on_settings 20 | tester = configure( 21 | git_user_name: "ahoy user", 22 | git_user_email: "hello@test.biz" 23 | ) 24 | tester.run_task("git:config") 25 | assert_equal( 26 | [ 27 | "git config --global user.name ahoy\\ user", 28 | "git config --global user.email hello@test.biz" 29 | ], 30 | tester.executed_scripts 31 | ) 32 | end 33 | 34 | def test_create_release_uses_branch_if_specified 35 | tester = configure(git_branch: "develop") 36 | tester.run_task("git:create_release") 37 | assert_equal( 38 | "cd /repo && git archive develop | tar -x -f - -C /app", 39 | tester.executed_scripts.grep(/git archive/).first 40 | ) 41 | end 42 | 43 | def test_create_release_uses_ref_if_specified 44 | tester = configure(git_branch: nil, git_ref: "a944898") 45 | tester.run_task("git:create_release") 46 | assert_equal( 47 | "cd /repo && git archive a944898 | tar -x -f - -C /app", 48 | tester.executed_scripts.grep(/git archive/).first 49 | ) 50 | end 51 | 52 | def test_create_release_uses_ref_if_both_branch_and_ref_specified 53 | tester = configure(git_branch: "main", git_ref: "a944898") 54 | tester.run_task("git:create_release") 55 | assert_equal( 56 | "cd /repo && git archive a944898 | tar -x -f - -C /app", 57 | tester.executed_scripts.grep(/git archive/).first 58 | ) 59 | assert_equal(<<~ERROR, tester.stderr) 60 | WARNING: :git_ref (a944898) and :git_branch (main) are both specified. Ignoring :git_branch. 61 | ERROR 62 | end 63 | 64 | def test_create_release_raises_error_if_branch_and_ref_both_nil 65 | tester = configure(git_branch: nil, git_ref: nil) 66 | assert_raises(Tomo::Runtime::SettingsRequiredError) do 67 | tester.run_task("git:create_release") 68 | end 69 | end 70 | 71 | private 72 | 73 | def configure(settings={}) 74 | defaults = { 75 | git_env: {}, 76 | git_repo_path: "/repo", 77 | release_path: "/app" 78 | } 79 | settings = defaults.merge(settings) 80 | Tomo::Testing::MockPluginTester.new("git", settings:) 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /test/tomo/plugin/rails/helpers_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "tomo/plugin/rails" 5 | 6 | class Tomo::Plugin::Rails::HelpersTest < Minitest::Test 7 | def test_rake_runs_bundle_exec_rake_in_current_path 8 | tester = Tomo::Testing::MockPluginTester.new("bundler", "rails", settings: { current_path: "/app/current" }) 9 | tester.call_helper(:rake, "db:migrate") 10 | assert_equal("cd /app/current && bundle exec rake db:migrate", tester.executed_script) 11 | end 12 | 13 | def test_thor_runs_bundle_exec_thor_in_current_path 14 | tester = Tomo::Testing::MockPluginTester.new("bundler", "rails", settings: { current_path: "/app/current" }) 15 | tester.call_helper(:thor, "user:create") 16 | assert_equal("cd /app/current && bundle exec thor user:create", tester.executed_script) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/tomo/plugin/rails/tasks_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "tomo/plugin/rails" 5 | 6 | class Tomo::Plugin::Rails::TasksTest < Minitest::Test 7 | def test_db_console 8 | tester = Tomo::Testing::MockPluginTester.new( 9 | "bundler", "rails", settings: { current_path: "/app/current" } 10 | ) 11 | assert_raises(Tomo::Testing::MockedExecError) { tester.run_task("rails:db_console") } 12 | assert_equal("cd /app/current && bundle exec rails dbconsole --include-password", tester.executed_script) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/tomo/runtime/settings_interpolation_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class Tomo::Runtime::SettingsInterpolationTest < Minitest::Test 6 | def test_interpolates_settings 7 | interpolated = interpolate( 8 | application: "test", 9 | deploy_to: "/var/www/%{application}", 10 | current_path: "%{deploy_to}/current", 11 | application_json_path: "%{deploy_to}/%{application}.json" 12 | ) 13 | assert_equal( 14 | { 15 | application: "test", 16 | deploy_to: "/var/www/test", 17 | current_path: "/var/www/test/current", 18 | application_json_path: "/var/www/test/test.json" 19 | }, 20 | interpolated 21 | ) 22 | end 23 | 24 | def test_raises_on_unknown_setting 25 | assert_raises(KeyError) do 26 | interpolate(deploy_to: "/var/www/%{application}") 27 | end 28 | end 29 | 30 | def test_raises_on_circular_dependency 31 | exception = assert_raises(RuntimeError) do 32 | interpolate( 33 | application: "default", 34 | deploy_to: "%{current_path}/%{application}", 35 | current_path: "%{deploy_to}/current" 36 | ) 37 | end 38 | assert_match("Circular dependency detected in settings: deploy_to -> current_path -> deploy_to", exception.message) 39 | end 40 | 41 | def test_no_longer_supports_old_syntax 42 | interpolated = interpolate( 43 | application: "default", 44 | deploy_to: "/var/www/%" 45 | ) 46 | assert_equal({ application: "default", deploy_to: "/var/www/% " }, interpolated) 47 | end 48 | 49 | private 50 | 51 | def interpolate(settings) 52 | Tomo::Runtime::SettingsInterpolation.interpolate(settings) 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/tomo/runtime_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class Tomo::RuntimeTest < Minitest::Test 6 | def test_deploy_raises_if_no_deploy_tasks 7 | runtime = Tomo::Configuration.new.build_runtime 8 | assert_raises(Tomo::Runtime::NoTasksError) do 9 | runtime.deploy! 10 | end 11 | end 12 | 13 | def test_setup_raises_if_no_setup_tasks 14 | runtime = Tomo::Configuration.new.build_runtime 15 | assert_raises(Tomo::Runtime::NoTasksError) do 16 | runtime.setup! 17 | end 18 | end 19 | 20 | def test_local_user_reads_user_env_var 21 | with_env(USER: "foo") do 22 | assert_equal("foo", local_user) 23 | end 24 | end 25 | 26 | def test_local_user_reads_username_env_var 27 | with_env(USER: nil, USERNAME: "bar") do 28 | assert_equal("bar", local_user) 29 | end 30 | end 31 | 32 | def test_local_user_reads_whoami_output 33 | with_env(USER: nil, USERNAME: nil) do 34 | with_whoami_mock("baz\n") do 35 | assert_equal("baz", local_user) 36 | end 37 | end 38 | end 39 | 40 | def test_local_user_gracefully_handles_whoami_failure 41 | with_env(USER: nil, USERNAME: nil) do 42 | with_whoami_mock(Errno::ENOENT) do 43 | assert_nil(local_user) 44 | end 45 | end 46 | end 47 | 48 | private 49 | 50 | def local_user 51 | runtime = Tomo::Testing::MockPluginTester.new.send(:runtime) 52 | plan = runtime.execution_plan_for([]) 53 | plan.settings.fetch(:local_user) 54 | end 55 | 56 | def with_env(mock_env) 57 | orig_env = ENV.to_h.dup 58 | mock_env.each do |key, value| 59 | ENV[key.to_s] = value 60 | end 61 | yield 62 | ensure 63 | orig_env.each do |key, value| 64 | ENV[key.to_s] = value 65 | end 66 | end 67 | 68 | def with_whoami_mock(result, &) 69 | result_callable = ->(*) { result.is_a?(Exception) ? raise(result) : result } 70 | Tomo::Runtime.stub(:`, result_callable, &) 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /test/tomo/shell_builder_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class Tomo::ShellBuilderTest < Minitest::Test 6 | def test_raw_preserves_string_when_shellescaped 7 | raw_string = Tomo::ShellBuilder.raw("$HOME") 8 | assert_equal("$HOME", raw_string.shellescape) 9 | end 10 | 11 | def test_raw_works_with_frozen_strings 12 | raw_string = Tomo::ShellBuilder.raw("$HOME") 13 | assert_equal("$HOME", raw_string.shellescape) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/tomo/task_api_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class Tomo::TaskAPITest < Minitest::Test 6 | Subject = Struct.new(:context) 7 | Subject.include Tomo::TaskAPI 8 | 9 | def test_merge_template_with_absolute_path 10 | abs_path = File.expand_path("../fixtures/template.erb", __dir__) 11 | subject = configure(application: "test-app") 12 | merged = subject.send(:merge_template, abs_path) 13 | assert_equal("Hello, test-app!\n", merged) 14 | end 15 | 16 | def test_merge_template_with_path_relative_to_config 17 | config_path = File.expand_path("../../.tomo/config.rb", __dir__) 18 | rel_path = "../test/fixtures/template.erb" 19 | subject = configure(application: "test-app", tomo_config_file_path: config_path) 20 | merged = subject.send(:merge_template, rel_path) 21 | assert_equal("Hello, test-app!\n", merged) 22 | end 23 | 24 | def test_merge_template_raises_on_file_not_found 25 | subject = configure 26 | assert_raises(Tomo::Runtime::TemplateNotFoundError) do 27 | subject.send(:merge_template, "path_does_not_exist") 28 | end 29 | end 30 | 31 | private 32 | 33 | def configure(settings={}) 34 | defaults = { tomo_config_file_path: nil } 35 | Subject.new(Tomo::Runtime::Context.new(defaults.merge(settings))) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /tomo.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/tomo/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "tomo" 7 | spec.version = Tomo::VERSION 8 | spec.authors = ["Matt Brictson"] 9 | spec.email = ["opensource@mattbrictson.com"] 10 | 11 | spec.summary = "A friendly CLI for deploying Rails apps ✨" 12 | spec.description = 13 | "Tomo is a feature-rich deployment tool that contains everything you need to deploy a basic Rails app out of the " \ 14 | "box. It has an opinionated, production-tested set of defaults, but is easily extensible via a well-documented " \ 15 | "plugin system. Unlike other Ruby-based deployment tools, tomo’s friendly command-line interface and task system " \ 16 | "do not rely on Rake." 17 | 18 | spec.homepage = "https://github.com/mattbrictson/tomo" 19 | spec.license = "MIT" 20 | spec.required_ruby_version = ">= 3.1" 21 | 22 | spec.metadata = { 23 | "bug_tracker_uri" => "https://github.com/mattbrictson/tomo/issues", 24 | "changelog_uri" => "https://github.com/mattbrictson/tomo/releases", 25 | "source_code_uri" => "https://github.com/mattbrictson/tomo", 26 | "homepage_uri" => spec.homepage, 27 | "documentation_uri" => "https://tomo.mattbrictson.com/", 28 | "rubygems_mfa_required" => "true" 29 | } 30 | 31 | # Specify which files should be added to the gem when it is released. 32 | spec.files = Dir.glob(%w[LICENSE.txt README.md {exe,lib}/**/*]).reject { |f| File.directory?(f) } 33 | spec.bindir = "exe" 34 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 35 | spec.require_paths = ["lib"] 36 | end 37 | --------------------------------------------------------------------------------