├── .gitignore ├── .rspec ├── .rubocop.yml ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── exe └── traker ├── lib ├── generators │ └── traker │ │ ├── models_generator.rb │ │ ├── rspec_generator.rb │ │ └── templates │ │ ├── migrations │ │ └── 20200930011917_add_traker_tasks.rb │ │ └── spec │ │ └── lib │ │ └── traker_spec.rb ├── traker.rb └── traker │ ├── cli.rb │ ├── config.rb │ ├── instrumenter.rb │ ├── override.rake │ ├── service.rb │ ├── task.rb │ └── version.rb ├── spec ├── lib │ └── traker │ │ ├── cli_spec.rb │ │ └── service_spec.rb ├── spec_helper.rb ├── support │ ├── .invalid_traker.yml │ ├── .traker.yml │ ├── database.rb │ └── traker.rake ├── traker │ └── config_spec.rb └── traker_spec.rb └── traker.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | *.gem 10 | 11 | # rspec failure tracking 12 | .rspec_status 13 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | NewCops: enable 3 | TargetRubyVersion: 2.4.0 4 | 5 | Layout/MultilineMethodCallIndentation: 6 | Enabled: false 7 | 8 | Lint/InheritException: 9 | Enabled: false 10 | 11 | Style/MultilineTernaryOperator: 12 | Enabled: false 13 | 14 | Style/GuardClause: 15 | Enabled: false 16 | 17 | Style/ClassAndModuleChildren: 18 | Enabled: false 19 | 20 | Style/ParallelAssignment: 21 | Enabled: false 22 | 23 | Style/IfUnlessModifier: 24 | Enabled: false 25 | 26 | Metrics: 27 | Enabled: false 28 | 29 | Naming/PredicateName: 30 | Enabled: false 31 | 32 | Layout/LineLength: 33 | Max: 170 34 | 35 | Layout/EmptyLineBetweenDefs: 36 | Enabled: false 37 | 38 | Style/Documentation: 39 | Enabled: true 40 | Exclude: 41 | - lib/traker.rb 42 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | sudo: false 3 | language: ruby 4 | cache: bundler 5 | rvm: 6 | - 2.5.3 7 | before_install: gem install bundler -v 2.0.1 8 | 9 | install: bundle install 10 | 11 | script: 12 | - rubocop 13 | - bundle exec rspec 14 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at posadchiy@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in traker.gemspec 6 | gemspec 7 | 8 | group :test do 9 | gem 'climate_control' 10 | gem 'database_cleaner' 11 | gem 'pry' 12 | gem 'rb-readline' 13 | gem 'rspec' 14 | gem 'rubocop', '~> 0.62' 15 | gem 'sqlite3', '~> 1.3', '< 1.4' 16 | end 17 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | traker (0.1.1) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | actioncable (5.2.4.4) 10 | actionpack (= 5.2.4.4) 11 | nio4r (~> 2.0) 12 | websocket-driver (>= 0.6.1) 13 | actionmailer (5.2.4.4) 14 | actionpack (= 5.2.4.4) 15 | actionview (= 5.2.4.4) 16 | activejob (= 5.2.4.4) 17 | mail (~> 2.5, >= 2.5.4) 18 | rails-dom-testing (~> 2.0) 19 | actionpack (5.2.4.4) 20 | actionview (= 5.2.4.4) 21 | activesupport (= 5.2.4.4) 22 | rack (~> 2.0, >= 2.0.8) 23 | rack-test (>= 0.6.3) 24 | rails-dom-testing (~> 2.0) 25 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 26 | actionview (5.2.4.4) 27 | activesupport (= 5.2.4.4) 28 | builder (~> 3.1) 29 | erubi (~> 1.4) 30 | rails-dom-testing (~> 2.0) 31 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 32 | activejob (5.2.4.4) 33 | activesupport (= 5.2.4.4) 34 | globalid (>= 0.3.6) 35 | activemodel (5.2.4.4) 36 | activesupport (= 5.2.4.4) 37 | activerecord (5.2.4.4) 38 | activemodel (= 5.2.4.4) 39 | activesupport (= 5.2.4.4) 40 | arel (>= 9.0) 41 | activestorage (5.2.4.4) 42 | actionpack (= 5.2.4.4) 43 | activerecord (= 5.2.4.4) 44 | marcel (~> 0.3.1) 45 | activesupport (5.2.4.4) 46 | concurrent-ruby (~> 1.0, >= 1.0.2) 47 | i18n (>= 0.7, < 2) 48 | minitest (~> 5.1) 49 | tzinfo (~> 1.1) 50 | arel (9.0.0) 51 | ast (2.4.1) 52 | builder (3.2.4) 53 | climate_control (0.2.0) 54 | coderay (1.1.3) 55 | concurrent-ruby (1.1.8) 56 | crass (1.0.6) 57 | database_cleaner (1.8.5) 58 | diff-lcs (1.4.4) 59 | erubi (1.9.0) 60 | globalid (0.4.2) 61 | activesupport (>= 4.2.0) 62 | i18n (1.8.10) 63 | concurrent-ruby (~> 1.0) 64 | loofah (2.7.0) 65 | crass (~> 1.0.2) 66 | nokogiri (>= 1.5.9) 67 | mail (2.7.1) 68 | mini_mime (>= 0.1.1) 69 | marcel (0.3.3) 70 | mimemagic (~> 0.3.2) 71 | method_source (1.0.0) 72 | mimemagic (0.3.10) 73 | nokogiri (~> 1) 74 | rake 75 | mini_mime (1.0.2) 76 | mini_portile2 (2.4.0) 77 | minitest (5.14.4) 78 | nio4r (2.5.4) 79 | nokogiri (1.10.10) 80 | mini_portile2 (~> 2.4.0) 81 | parallel (1.19.2) 82 | parser (2.7.1.5) 83 | ast (~> 2.4.1) 84 | pry (0.13.1) 85 | coderay (~> 1.1) 86 | method_source (~> 1.0) 87 | rack (2.2.3) 88 | rack-test (1.1.0) 89 | rack (>= 1.0, < 3) 90 | rails (5.2.4.4) 91 | actioncable (= 5.2.4.4) 92 | actionmailer (= 5.2.4.4) 93 | actionpack (= 5.2.4.4) 94 | actionview (= 5.2.4.4) 95 | activejob (= 5.2.4.4) 96 | activemodel (= 5.2.4.4) 97 | activerecord (= 5.2.4.4) 98 | activestorage (= 5.2.4.4) 99 | activesupport (= 5.2.4.4) 100 | bundler (>= 1.3.0) 101 | railties (= 5.2.4.4) 102 | sprockets-rails (>= 2.0.0) 103 | rails-dom-testing (2.0.3) 104 | activesupport (>= 4.2.0) 105 | nokogiri (>= 1.6) 106 | rails-html-sanitizer (1.3.0) 107 | loofah (~> 2.3) 108 | railties (5.2.4.4) 109 | actionpack (= 5.2.4.4) 110 | activesupport (= 5.2.4.4) 111 | method_source 112 | rake (>= 0.8.7) 113 | thor (>= 0.19.0, < 2.0) 114 | rainbow (3.0.0) 115 | rake (12.3.3) 116 | rb-readline (0.5.5) 117 | regexp_parser (1.8.1) 118 | rexml (3.2.4) 119 | rspec (3.9.0) 120 | rspec-core (~> 3.9.0) 121 | rspec-expectations (~> 3.9.0) 122 | rspec-mocks (~> 3.9.0) 123 | rspec-core (3.9.2) 124 | rspec-support (~> 3.9.3) 125 | rspec-expectations (3.9.2) 126 | diff-lcs (>= 1.2.0, < 2.0) 127 | rspec-support (~> 3.9.0) 128 | rspec-mocks (3.9.1) 129 | diff-lcs (>= 1.2.0, < 2.0) 130 | rspec-support (~> 3.9.0) 131 | rspec-support (3.9.3) 132 | rubocop (0.92.0) 133 | parallel (~> 1.10) 134 | parser (>= 2.7.1.5) 135 | rainbow (>= 2.2.2, < 4.0) 136 | regexp_parser (>= 1.7) 137 | rexml 138 | rubocop-ast (>= 0.5.0) 139 | ruby-progressbar (~> 1.7) 140 | unicode-display_width (>= 1.4.0, < 2.0) 141 | rubocop-ast (0.7.1) 142 | parser (>= 2.7.1.5) 143 | ruby-progressbar (1.10.1) 144 | sprockets (4.0.2) 145 | concurrent-ruby (~> 1.0) 146 | rack (> 1, < 3) 147 | sprockets-rails (3.2.2) 148 | actionpack (>= 4.0) 149 | activesupport (>= 4.0) 150 | sprockets (>= 3.0.0) 151 | sqlite3 (1.3.13) 152 | thor (1.0.1) 153 | thread_safe (0.3.6) 154 | tzinfo (1.2.9) 155 | thread_safe (~> 0.1) 156 | unicode-display_width (1.7.0) 157 | websocket-driver (0.7.3) 158 | websocket-extensions (>= 0.1.0) 159 | websocket-extensions (0.1.5) 160 | 161 | PLATFORMS 162 | ruby 163 | 164 | DEPENDENCIES 165 | activerecord (>= 5.2) 166 | bundler (~> 2.0) 167 | climate_control 168 | database_cleaner 169 | pry 170 | rails (~> 5.0) 171 | rake (~> 12.3.3) 172 | rb-readline 173 | rspec 174 | rubocop (~> 0.62) 175 | sqlite3 (~> 1.3, < 1.4) 176 | traker! 177 | 178 | BUNDLED WITH 179 | 2.0.1 180 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 pavloo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Traker 2 | 3 | Traker is a *Rake task tracker for Rails applications*. When integrated, it keeps track of rake tasks that have been run and stores that information in database. 4 | 5 | ## Installation 6 | 7 | Add this line to your application's Gemfile: 8 | 9 | ```ruby 10 | gem 'traker' 11 | ``` 12 | 13 | And then execute: 14 | 15 | $ bundle 16 | 17 | ## Usage 18 | 1. Add the next snippet to your `Rakefile` after `Rails.application.load_tasks` 19 | 20 | ```ruby 21 | # Rails.application.load_tasks has to be above of the code we add 22 | 23 | spec = Gem::Specification.find_by_name 'traker' 24 | load File.join(spec.gem_dir, 'lib', 'traker', 'override.rake') 25 | ``` 26 | 27 | 2. Run generator 28 | 29 | ``` ruby 30 | rails g traker:models migration 31 | ``` 32 | 33 | 3. Run `rake db:migrate` 34 | 4. Set `TRAKER_ENV` environment variable (see below) 35 | 36 | ### Configuration file 37 | Traker with only care about tasks that are specified in it's configuration file `.traker.yml`. Here is the example of such a file: 38 | 39 | ``` yml 40 | environments: 41 | dev: 42 | - name: traker:test1 43 | notes: Some fancy description here 44 | 45 | - name: traker:test2 46 | 47 | stg: 48 | - name: traker:test1 49 | ``` 50 | Schema breakdown: 51 | * `environments.` - the name of the environment (`TRAKER_ENV`) the task has to be run against 52 | * `environments..name` - the name of the rake task in namespace:name format 53 | * `environments..notes` - some information that might be important when the task is run (should be different from rake task's description) 54 | 55 | ### Testing 56 | 57 | Traker allows to keep your `.traker.yml` configuration file consistent and valid. Run a generator to install rspec related files: 58 | 59 | ``` ruby 60 | rails g traker:rspec test 61 | ``` 62 | 63 | ### CLI 64 | Traker exposes a command line interface. Commands implemented so far: 65 | 66 | 1) `traker list` - lists pending (those that haven't been run in the current environment) tasks. Available options: 67 | * `-a, --all` - list all tasks in the current environment 68 | 69 | ## Development 70 | 71 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 72 | 73 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 74 | 75 | ## Contributing 76 | 77 | Bug reports and pull requests are welcome on GitHub at https://github.com/pavloo/traker. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 78 | 79 | ## License 80 | 81 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 82 | 83 | ## Code of Conduct 84 | 85 | Everyone interacting in the Traker project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/pavloo/traker/blob/master/CODE_OF_CONDUCT.md). 86 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task default: :spec 9 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'traker' 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require 'irb' 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /exe/traker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # frozen_string_literal: true 4 | 5 | require File.expand_path(File.join(Dir.pwd, 'config/environment')) 6 | require 'traker' 7 | 8 | Traker::CLI.new.run(ARGV) 9 | -------------------------------------------------------------------------------- /lib/generators/traker/models_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails/generators/active_record' 4 | 5 | module Traker 6 | module Generators 7 | # A rails generator that generates model-related files in the host 8 | # Ruby On Rails project 9 | class ModelsGenerator < ActiveRecord::Generators::Base 10 | source_root File.expand_path('templates', __dir__) 11 | 12 | def generate_model 13 | migration_template 'migrations/20200930011917_add_traker_tasks.rb', 'db/migrate/add_traker_tasks.rb' 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/generators/traker/rspec_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails/generators/active_record' 4 | 5 | module Traker 6 | module Generators 7 | # A rails generator that generates rspec-related files in the host 8 | # Ruby On Rails project 9 | class RspecGenerator < ActiveRecord::Generators::Base 10 | source_root File.expand_path('templates', __dir__) 11 | 12 | def generate_spec 13 | template 'spec/lib/traker_spec.rb', 'spec/lib/traker_spec.rb' 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/generators/traker/templates/migrations/20200930011917_add_traker_tasks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # ActiveRecord migration that creates Traker::Task 4 | # table 5 | class AddTrakerTasks < ActiveRecord::Migration[5.2] 6 | def change 7 | create_table :traker_tasks do |t| 8 | t.string :name, null: false 9 | t.string :environment, null: false 10 | t.boolean :is_success 11 | t.integer :run_count, default: 0 12 | t.text :error 13 | t.datetime :started_at, null: false 14 | t.datetime :finished_at, null: false 15 | end 16 | 17 | add_index :traker_tasks, %i[name environment] 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/generators/traker/templates/spec/lib/traker_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | require 'traker' 5 | 6 | describe '.traker.yml' do 7 | it 'is valid' do 8 | Rails.application.load_tasks 9 | expect { Traker::Config.load.validate!(Rake::Task.tasks) }.not_to raise_error 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/traker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'traker/version' 4 | require 'traker/config' 5 | require 'traker/instrumenter' 6 | require 'traker/task' 7 | require 'traker/service' 8 | require 'traker/cli' 9 | 10 | module Traker 11 | end 12 | -------------------------------------------------------------------------------- /lib/traker/cli.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'optparse' 4 | 5 | module Traker 6 | # Traker's CLI interfase 7 | class CLI 8 | SUBTEXT = < OptionParser.new do |opts| 27 | opts.banner = 'Usage: list [options]' 28 | opts.on('-a', '--all', 'list all tasks') 29 | end 30 | } 31 | end 32 | 33 | def run(argv) 34 | options = {} 35 | @main.order!(argv, into: options) 36 | if options[:version] 37 | puts Traker::VERSION 38 | return 39 | end 40 | 41 | subcommand = argv.shift 42 | subcommand_options = {} 43 | @subcommands[subcommand]&.order!(argv, into: subcommand_options) 44 | 45 | service = Traker::Service.new 46 | 47 | case subcommand 48 | when SUBCOMMANDS[:list] 49 | if subcommand_options[:all] 50 | print service.tasks.join("\n") 51 | else 52 | print service.pending_tasks.join("\n") 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/traker/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'yaml' 4 | require 'rails' 5 | 6 | module Traker 7 | # Represents Traker configuration. 8 | class Config 9 | PATH = '.traker.yml' 10 | 11 | class InvalidTasks < StandardError 12 | end 13 | 14 | def self.load 15 | Config.new(File.join(::Rails.root, PATH)) 16 | end 17 | 18 | def initialize(file) 19 | yml = YAML.safe_load(File.read(file)) 20 | @environments = yml['environments'] 21 | rescue Psych::SyntaxError => e 22 | puts "[TRAKER] unable to load config file: #{e}" 23 | @environments = {} 24 | end 25 | 26 | def env 27 | @env ||= ENV.fetch('TRAKER_ENV', 'default') 28 | end 29 | 30 | def tasks 31 | @environments[env] || [] 32 | end 33 | 34 | def validate!(available_tasks) 35 | available_task_names = available_tasks.map { |t| extract_task_name(t[:name]) } 36 | 37 | @environments.each do |_, tasks| 38 | task_names = (tasks || []).map { |t| extract_task_name(t['name']) } 39 | invalid_tasks = task_names - available_task_names 40 | 41 | if invalid_tasks.any? 42 | raise InvalidTasks, "#{PATH} contains invalid tasks: #{invalid_tasks.join(',')}" 43 | end 44 | end 45 | end 46 | 47 | private 48 | 49 | # Extracts task name without arguments 50 | def extract_task_name(str) 51 | matches = str.match(/^(?[^\[\]]*)(?:(\[.*\])?)$/) # Task name with optional parameters 52 | if matches.try(:names).blank? 53 | raise InvalidTasks, "#{PATH} contains a bad formatted task: #{str}" 54 | else 55 | matches[:name] 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/traker/instrumenter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Traker 4 | # Wraps array of rake tasks, and auguments each one of them 5 | # with Traker features 6 | class Instrumenter 7 | attr_accessor :tasks, :config 8 | 9 | def initialize(tasks) 10 | @tasks = tasks 11 | @config = Traker::Config.load 12 | end 13 | 14 | def instrument 15 | tasks.each do |t| 16 | task_name = t.name 17 | 18 | next unless tasks_to_be_run.map { |task| task['name'] }.include?(task_name) 19 | 20 | handler = proc do |&block| 21 | record = Traker::Task.find_or_initialize_by(name: task_name, environment: config.env) 22 | 23 | record.started_at = DateTime.now 24 | record.is_success = true 25 | 26 | begin 27 | block.call 28 | record.run_count += 1 29 | rescue StandardError => e 30 | record.is_success = false 31 | record.error = e.backtrace.first 32 | raise e 33 | ensure 34 | record.finished_at = DateTime.now 35 | record.save! 36 | end 37 | end 38 | 39 | yield task_name, handler 40 | end 41 | end 42 | 43 | private 44 | 45 | def tasks_to_be_run 46 | config.tasks 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/traker/override.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../traker' 4 | 5 | Traker::Instrumenter.new(Rake::Task.tasks).instrument do |task_name, handle_task| 6 | original_task = Rake.application.instance_variable_get('@tasks').delete(task_name) 7 | task_prerequisites = original_task.prerequisites | ['environment'] 8 | task_description = original_task.full_comment 9 | 10 | desc task_description 11 | task task_name => task_prerequisites do 12 | handle_task.call do 13 | original_task.invoke 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/traker/service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'traker/task' 4 | 5 | module Traker 6 | # Service that encapsulates main Traker API 7 | class Service 8 | def initialize 9 | @config = Traker::Config.load 10 | end 11 | 12 | def pending_tasks 13 | records = Traker::Task.where(name: tasks, environment: @config.env) 14 | actual = tasks.each_with_object({}) do |name, hash| 15 | record = records.find { |r| r.name == name } 16 | hash[name] = record ? record.run_count : 0 17 | end 18 | 19 | pending = [] 20 | tasks.each do |name| 21 | actual[name] -= 1 22 | next if actual[name] >= 0 23 | 24 | pending << name 25 | end 26 | 27 | pending 28 | end 29 | 30 | def tasks 31 | @tasks ||= @config.tasks.map { |t| t['name'] } 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/traker/task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_record' 4 | 5 | module Traker 6 | # Represents a rake task that have been run and logged by Traker 7 | class Task < ::ActiveRecord::Base 8 | self.table_name_prefix = 'traker_' 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/traker/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Traker 4 | VERSION = '0.1.1' 5 | end 6 | -------------------------------------------------------------------------------- /spec/lib/traker/cli_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Traker::CLI do 4 | context 'list subcommand' do 5 | context 'return tasks' do 6 | before do 7 | allow_any_instance_of( 8 | Traker::Service 9 | ).to receive(:pending_tasks).and_return(['traker:test1', 'traker:test2']) 10 | allow_any_instance_of( 11 | Traker::Service 12 | ).to receive(:tasks).and_return(['traker:test1']) 13 | end 14 | 15 | it 'returns list of pending tasks' do 16 | argv = ['list'] 17 | output = "traker:test1\ntraker:test2" 18 | expect { described_class.new.run(argv) }.to output(output).to_stdout 19 | end 20 | 21 | it 'returns list of all tasks' do 22 | argv = ['list', '-a'] 23 | output = 'traker:test1' 24 | expect { described_class.new.run(argv) }.to output(output).to_stdout 25 | end 26 | end 27 | end 28 | 29 | context 'main options' do 30 | it 'returns version' do 31 | argv = ['-v'] 32 | output = "#{Traker::VERSION}\n" 33 | expect { described_class.new.run(argv) }.to output(output).to_stdout 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/lib/traker/service_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Traker::Service do 4 | context 'pending tasks' do 5 | it 'returns list of new tasks' do 6 | expect(described_class.new.pending_tasks).to eq(['traker:rake_success', 'traker:rake_success1']) 7 | end 8 | 9 | it 'excludes task that has been already run' do 10 | instrument_rake_tasks! 11 | Rake::Task['traker:rake_success'].execute 12 | 13 | expect(described_class.new.pending_tasks).to eq(['traker:rake_success1']) 14 | end 15 | 16 | it 'returns empty list' do 17 | instrument_rake_tasks! 18 | Rake::Task['traker:rake_success'].execute 19 | Rake::Task['traker:rake_success1'].execute 20 | 21 | expect(described_class.new.pending_tasks).to be_empty 22 | end 23 | 24 | it 'returns list when task appears more than once' do 25 | with_modified_env TRAKER_ENV: 'stg' do 26 | instrument_rake_tasks! 27 | Rake::Task['traker:rake_success'].execute 28 | 29 | expect(described_class.new.pending_tasks).to eq(['traker:rake_success', 'traker:rake_success1']) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | require 'active_record' 5 | require 'database_cleaner' 6 | require 'support/database' 7 | require 'rake' 8 | require 'rails' 9 | require 'climate_control' 10 | require 'traker' 11 | 12 | RSpec.configure do |config| 13 | # Enable flags like --only-failures and --next-failure 14 | config.example_status_persistence_file_path = '.rspec_status' 15 | 16 | # Disable RSpec exposing methods globally on `Module` and `main` 17 | config.disable_monkey_patching! 18 | 19 | config.expect_with :rspec do |c| 20 | c.syntax = :expect 21 | end 22 | 23 | config.before(:suite) do 24 | DatabaseCleaner.strategy = :transaction 25 | end 26 | 27 | config.before(:each) do 28 | allow(::Rails).to receive(:root).and_return(Pathname.new(File.join(__dir__, 'support'))) 29 | end 30 | 31 | config.around(:each) do |example| 32 | if ActiveRecord::Base.connected? 33 | DatabaseCleaner.cleaning { example.run } 34 | else 35 | example.run 36 | end 37 | end 38 | end 39 | 40 | def with_modified_env(options, &block) 41 | ClimateControl.modify(options, &block) 42 | end 43 | 44 | def instrument_rake_tasks! 45 | Rake::Task.tasks.each do |task| 46 | name = task.name 47 | Rake.application.instance_variable_get('@tasks').delete(name) 48 | end 49 | Rake.load_rakefile(File.join(__dir__, 'support', 'traker.rake')) 50 | Rake.load_rakefile(File.join(__dir__, '..', 'lib', 'traker', 'override.rake')) 51 | end 52 | -------------------------------------------------------------------------------- /spec/support/.invalid_traker.yml: -------------------------------------------------------------------------------- 1 | bad yml 2 | 3 | name: 4 | -------------------------------------------------------------------------------- /spec/support/.traker.yml: -------------------------------------------------------------------------------- 1 | environments: 2 | dev: 3 | - name: traker:rake_success 4 | - name: traker:rake_fail 5 | default: 6 | - name: traker:rake_success 7 | - name: traker:rake_success1 8 | stg: 9 | - name: traker:rake_success 10 | - name: traker:rake_success 11 | - name: traker:rake_success1 12 | -------------------------------------------------------------------------------- /spec/support/database.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_record' 4 | 5 | ActiveRecord::Migration.verbose = true 6 | ActiveRecord::Base.logger = nil 7 | ActiveRecord::Base.establish_connection( 8 | adapter: 'sqlite3', 9 | database: ':memory:' 10 | ) 11 | 12 | ActiveRecord::Schema.define do 13 | ActiveRecord::MigrationContext.new( 14 | File.join(__dir__, '..', '..', 'lib', 'generators', 'traker', 'templates', 'migrations') 15 | ).migrate 16 | end 17 | -------------------------------------------------------------------------------- /spec/support/traker.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | namespace :traker do 4 | # stubbed version of Rails's "environment" task 5 | task :environment do 6 | end 7 | 8 | desc 'successful task' 9 | task :rake_success do 10 | puts 'Victory.' 11 | end 12 | 13 | desc 'another successful task' 14 | task :rake_success1 do 15 | puts 'Success.' 16 | end 17 | 18 | desc 'failing task' 19 | task :rake_fail do 20 | raise 'Failure.' 21 | end 22 | 23 | desc 'ignored task' 24 | task :rake_ignore do 25 | puts 'This task is just run and not support to be logged by Traker.' 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/traker/config_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Traker::Config do 4 | subject { Traker::Config.load } 5 | 6 | describe '.load' do 7 | it { is_expected.not_to be_blank } 8 | end 9 | 10 | describe '#initialize' do 11 | it 'does not fail if config has incorrect syntax' do 12 | expect { Traker::Config.new('./spec/support/.invalid_traker.yml') } 13 | .not_to raise_error 14 | end 15 | end 16 | 17 | describe '#env' do 18 | it 'returns default env' do 19 | expect(subject.env).to eq 'default' 20 | end 21 | 22 | it 'returns the modified env' do 23 | with_modified_env TRAKER_ENV: 'dev' do 24 | expect(subject.env).to eq 'dev' 25 | end 26 | end 27 | end 28 | 29 | describe '#tasks_to_be_run' do 30 | it 'returns default tasks to be run' do 31 | with_modified_env TRAKER_ENV: 'dev' do 32 | expect(subject.tasks) 33 | .to eq [{ 'name' => 'traker:rake_success' }, { 'name' => 'traker:rake_fail' }] 34 | end 35 | end 36 | end 37 | 38 | describe '#validate!' do 39 | let(:available_tasks) do 40 | end 41 | 42 | it 'raises if there is a task mismatch' do 43 | available_tasks = [ 44 | OpenStruct.new(name: 'task1'), 45 | OpenStruct.new(name: 'task2') 46 | ] 47 | expect { subject.validate!(available_tasks) } 48 | .to raise_error Traker::Config::InvalidTasks, '.traker.yml contains invalid tasks: traker:rake_success,traker:rake_fail' 49 | end 50 | 51 | it 'validates if tasks match' do 52 | available_tasks = [ 53 | OpenStruct.new(name: 'traker:rake_success1'), 54 | OpenStruct.new(name: 'traker:rake_success'), 55 | OpenStruct.new(name: 'traker:rake_fail') 56 | ] 57 | expect { subject.validate!(available_tasks) }.not_to raise_error 58 | end 59 | 60 | it 'validates if tasks with parameters match' do 61 | available_tasks = [ 62 | OpenStruct.new(name: 'traker:rake_success1[foo]'), 63 | OpenStruct.new(name: 'traker:rake_success'), 64 | OpenStruct.new(name: 'traker:rake_fail[foo,bar]') 65 | ] 66 | expect { subject.validate!(available_tasks) }.not_to raise_error 67 | end 68 | 69 | it 'raises if there is a task with bad formatted parameters' do 70 | available_tasks = [ 71 | OpenStruct.new(name: 'traker:rake_success1[foo'), 72 | OpenStruct.new(name: 'traker:rake_success'), 73 | OpenStruct.new(name: 'traker:rake_fail[]') 74 | ] 75 | expect { subject.validate!(available_tasks) } 76 | .to raise_error Traker::Config::InvalidTasks, '.traker.yml contains a bad formatted task: traker:rake_success1[foo' 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /spec/traker_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Traker do 4 | it 'has a version number' do 5 | expect(Traker::VERSION).not_to be nil 6 | end 7 | 8 | context 'task is in the config' do 9 | it 'logs successful task' do 10 | traker_env = 'dev' 11 | with_modified_env TRAKER_ENV: traker_env do 12 | instrument_rake_tasks! 13 | task_name = 'traker:rake_success' 14 | Rake::Task[task_name].execute 15 | 16 | expect(Traker::Task.count).to eq 1 17 | record = Traker::Task.first 18 | 19 | expect(record.name).to eq task_name 20 | expect(record.environment).to eq traker_env 21 | expect(record.is_success).to be(true) 22 | expect(record.error).to be_nil 23 | expect(record.run_count).to eq 1 24 | expect(record.started_at).to be_instance_of(Time) 25 | expect(record.finished_at).to be_instance_of(Time) 26 | end 27 | end 28 | 29 | it 'increases task count' do 30 | traker_env = 'dev' 31 | with_modified_env TRAKER_ENV: traker_env do 32 | instrument_rake_tasks! 33 | task_name = 'traker:rake_success' 34 | 35 | Rake::Task[task_name].execute 36 | expect(Traker::Task.count).to eq 1 37 | record = Traker::Task.first 38 | expect(record.run_count).to eq 1 39 | 40 | Rake::Task[task_name].execute 41 | expect(Traker::Task.count).to eq 1 42 | record.reload 43 | expect(record.run_count).to eq 2 44 | end 45 | end 46 | 47 | it 'ignores task with the same name but from different environment' do 48 | traker_env = 'prd' 49 | with_modified_env TRAKER_ENV: traker_env do 50 | instrument_rake_tasks! 51 | task_name = 'traker:rake_success' 52 | 53 | Rake::Task[task_name].execute 54 | expect(Traker::Task.count).to eq 0 55 | end 56 | end 57 | end 58 | 59 | context 'task is not in config' do 60 | it 'ignores task that is not in the config file' do 61 | instrument_rake_tasks! 62 | task_name = 'traker:rake_ignore' 63 | 64 | Rake::Task[task_name].execute 65 | expect(Traker::Task.count).to eq 0 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /traker.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'traker/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'traker' 9 | spec.version = Traker::VERSION 10 | spec.authors = ['pavloo'] 11 | spec.email = ['posadchiy@gmail.com'] 12 | 13 | spec.summary = 'Rake tasks tracker, Traker' 14 | spec.description = 'Track rake tasks in your Rails application.' 15 | spec.homepage = 'https://github.com/pavloo/traker' 16 | spec.license = 'MIT' 17 | 18 | # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' 19 | # to allow pushing to a single host or delete this section to allow pushing to any host. 20 | # if spec.respond_to?(:metadata) 21 | # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'" 22 | 23 | # spec.metadata["homepage_uri"] = spec.homepage 24 | # spec.metadata["source_code_uri"] = "https://github.com/pavloo/traker" 25 | # spec.metadata["changelog_uri"] = "https://github.com/pavloo/traker/CHANGELOG.md" 26 | # else 27 | # raise "RubyGems 2.0 or newer is required to protect against " \ 28 | # "public gem pushes." 29 | # end 30 | 31 | # Specify which files should be added to the gem when it is released. 32 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 33 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 34 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 35 | end 36 | spec.bindir = 'exe' 37 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 38 | spec.require_paths = ['lib'] 39 | spec.required_ruby_version = '>=2.4' 40 | 41 | spec.add_development_dependency 'bundler', '~> 2.0' 42 | spec.add_development_dependency 'rake', '~> 12.3.3' 43 | spec.add_development_dependency 'rspec', '~> 3.0' 44 | 45 | spec.add_development_dependency 'activerecord', '>= 5.2' 46 | spec.add_development_dependency 'rails', '~> 5.0' 47 | end 48 | --------------------------------------------------------------------------------