Task management with Asana, Quire and Trello

  • Aug 5

There are just so many task management tools out there. I have spend a lot of time trying some of them. Only until recently when my tasks become too many to handle by email and memory, I start to really think about which tool to use. Most of them can do collaboration by sharing tasks with others. Thus, it is not a really big issue to me. The reason I have not found a perfect solution is that task management is not just what it looks like.

To me, task management consists of three different things:

  1. Reminder: very simple task to remind me what to do, nothing more
  2. Planning: besides reminding, this requires some sort of organization of many smaller tasks
  3. Data collection: many tasks result data, which need to be kept and might create new tasks

The solution I have for now are:

Asana

Asana is a very popular task management for team. For each task, you can assign to other and attach files. Tasks can be grouped into projects and workspaces. But in general, tasks are organized as a list of tasks. Relationship between tasks is weakly linked by projects and tags. Therefore, I use it as a reminders. Just quickly type what to do today and tomorrow, set a due day, assign to myself or others. That's it. It allows me to remember every trivial things.

Quire

Quire is a very young task management tool on the market. What it is good is that tasks can be arranged in endless hierarchy. It is a relatively simple tool than Asana, but because of that, the website feels snappy. When do you need a hierarchical task manager ? Planning. Similar to outliner, I can have a big project diced into small tasks and track each one of them. Surely I can also put trivial reminders into it, but quickly the whole hierarchy becomes messy and it becomes hard to find important tasks from trivial ones. Thus, it is better to keep reminder-type task out of planning. Quire is good in outling the relationship between tasks. In another word, it is good for planning.

Trello

One big reason for people to keep using email is that they do not only handle tasks, but they also need a place to keep the data from task and comments on them. Although both Asana and Quire can have attachment, you need to download them before viewing. It is as inconvenient as email. Trello does a very good job in presenting data visually, less on task management. Therefore, I use it to keep the data of finished tasks and add comments on it. It is also a good tool to share data with others.

In the end, I use these three tools to keep things going. An email can be converted into any of them and make my inbox clean.

Group By Array in PostgreSQL

  • Jun 26

Previously we showed that you can implement tags with PostgreSQL array type. What if you want to count how many topics belong to a given tag ? You can still use the combination of group and count.

current_user.ownerships.group("unnest(ownerships.tags)").count

It will gives you a hash like this:

{"work"=>6, "family"=>1, "meeting"=>1, "2015"=>1}

Query on polymorphic association

  • Jun 23

Assume you have these association:

  1. a user has many likes
  2. a like is associated to either article or comment

Therefore, a Like model look like this:

class Like < ActiveRecord::Base
  belongs_to :user
  belongs_to :likable, polymorphic: true
end

Article and comment are all likable.

You can search content of article belonging to a given user like this:

Article.joins(likables: :user).where("users.id = ?", current_user.id)

What if you want to search both article and comment at the same time ? The key is LEFT JOIN.

You need to chain Like, Article and Comment together, then you can search on article and comment at the same time.

To chain them together, try this:

likes = Like.joins("LEFT OUTER JOIN articles on likes.likable_id = articles.id AND likes.likable_type = \'Article\'").joins("LEFT OUTER JOIN comments on likes.likable_id = comments.id AND likes.likable_type = \'Comment\'")

Now, both articles and comments are included, you can search easy:

likes.where("articles.title = ? OR comments.title = ?", "title", "title")

It works in ActiveRecord Relation and can be chained easily. For example, you can even eager-load likable

likes.where("articles.title = ? OR comments.title = ?", "title", "title").includes(:likable)

Then iterate likes to get all matched articles and comments.

Step-by-step Tutorial of Electron (Atom Shell), part I

  • Apr 27

I am quite new to Electron and cannot find a good tutorial for very beginner like me. So I decide to try and see how it works. My platform is Linux Mint.

First install the electron as suggested:

$ sudo npm install electron-prebuilt -g

It will install electron-v0.25.1-linux-x64.zip.

Then you can have a minimal hello_world example from this gist

$ git clone https://gist.github.com/427fe11ad6beb8b276fb.git helloworld
$ cd helloworld
$ electron .

Then a simple application starts !! It provides many built-in functions, even the developer tools. (Alt-Command-I).

Mithril Tips & Tricks 1

  • Apr 8

There are many javascript framework doing bi-directional data binding such as Angular.js. But many of them are too heavy to use with Rails. It creates a lot of redundancy. Fortunately, Mithril is an light-weight framework which suits Rails very well. Here is a CoffeeScript version of Mitrhil todo which you can start with. Just add a div#mithril-todo in one of your view and you are ready to go.

todo = {};
todo_element = null;

todo.Todo = (data) ->
  this.description = m.prop(data.description);
  this.done = m.prop(false);
  this

todo.TodoList = m.prop([])

todo.vm = (->
  vm = {}
  vm.init = ->
    vm.list = todo.TodoList;
    vm.description = m.prop("");

    vm.add = ->
      if vm.description()
        vm.list.push(new todo.Todo({description: vm.description()}));
        vm.description("");

  return vm
)()

todo.controller = ->
  todo.vm.init()

todo.view = ->
  return m("div", [
    m("ul", [
      todo.vm.list().map (task, index)-> 
        return m("li", [
            m("input[type=checkbox]", {onclick: m.withAttr("checked", task.done), 
checked: task.done()})
            m("span", {style: {textDecoration: task.done() ? "line-through" : "none"}}, task.description
()),
          ])
    ]),
    m("input", {onchange: m.withAttr("value", todo.vm.description), value: todo.vm.description()}),
    m("button", {onclick: todo.vm.add}, "Add")
  ]);


loadMithrilTodo = -> 
  todo_element = document.getElementById('mithril-todo')
  if todo_element
    m.module(todo_element, {controller: todo.controller, view: todo.view});
    todo_id = todo_element.getAttribute('data-todo');
    m.request({
      method: 'GET', 
      url: '/todos/'+todo_id+'.json',
      type: todo.Todo,
      unwrapSuccess: (response)->
        return response.todos;
      unwrapeError: (response)->
        return response.error;
    }).then(todo.TodoList).then((data) ->
      console.log(data);
    )

# initialize the application
$(document).ready(loadMithrilTodo);
$(document).on('page:load', loadMithrilTodo);

You might need some modification for your application. While it works fine, how do I sent the change of task back ? The action is here:

m("input[type=checkbox]", {onclick: m.withAttr("checked", task.done),

And you can easily substitute task.done with any function like this:

m("input[type=checkbox]", {onclick: m.withAttr("checked", todo.vm.toggle),
...
todo.vm = (->
  vm = {}
  vm.init = ->
    ...
    vm.toggle = (data) ->
      console.log(data);
  return vm
)()

But you will find that todo.vm.toggle will receive only true and false. How do you know which task is checked ?

You can find the voodoo like this:

m("input[type=checkbox]", {onclick: m.withAttr("checked", todo.vm.toggle.bind(this, task)),
...
todo.vm = (->
  vm = {}
  vm.init = ->
    ...
    vm.toggle = (task, data) ->
      task.done(data)
      # do something here
  return vm
)()

As you can see, you can not only set the task.done, but also have chance to do something else, such as post the result back to Rails.

Nitrous vs Koding

  • Mar 27

There are many choices of cloud development environment. Many emphasize on collaboration. For me, collaboration is easier throught some other tools such as git and slack. Thus, all I need is a machine which I can login with SSH. The IDE provided by cloud development environment usually do not suit me. Nitrous and Koding both provide SSH login at free plan.

I initially use Nitrous and earn some free N2O. So the total N2O is 235. It gives either a machine of 512MB/3.75G or 640MB/1.75G. 512MB is barely enough and I cannot have both Rails server and test at the same time. The machine is a little bit slow depending on the time of day. And from time to time, the restarting will hang. So I decide to give Koding a try.

Koding provides free 1G/3G plan, which is better than Nitrous. It gives a virtual machine to start with. Therefore, there are more freedom to do whatever you want, but unlike Nitrous, I have to set up many small things by myself. Nitrous indeed provides what it claim: "Claim your Ruby Development Box in 60 seconds". I spent half a day to get PostgreSQL working on Koding since I am not familiar with its configuration. But I can use rvm on Koding while there is not enough resources to do so on Nitrous. And Koding feels faster.

Koding is not flawless. First, it takes longer to boot up than Nitrous. If you only need to boot up once per day, it will be fine. Unfortunately, it will automatically turn off after 1-hour inactivity. And from time to time, I will be forced to log out because it thought I were not using it while I was actually doing something. The activity detection of Koding is not very good. So remember to save your files more frequently. And it use dynamics IP for its virtual machine (domain name is fixed) and sometimes I cannot connect to it due to DNS problem.

So now, I keep both of them but spent more time on Koding for a Rails application development. If Koding kicks me out too often, I might have to go back to Nitrous.

Rails cache on collection

  • Mar 24

Rails fragment caching on collection is not very straight forward. You need to consider the deletion and modification of any record in collection. Rails do provide a basic way to represent a collection:

module ProductsHelper
  def cache_key_for_products
    count          = Product.count
    max_updated_at = Product.maximum(:updated_at).try(:utc).try(:to_s, :number)
    "products/all-#{count}-#{max_updated_at}"
  end
end

Then use it in the products/index.html.erb

<% cache(cache_key_for_products) do %>
  All available products:
<% end %>

We can combine these two into one:

cache [Product.count, Product.maximum(:updated_at)]

Cache can take array and will automatically handle the datetime type.

But we usually have products associated with user and we need to cache collection for each user. Thus, it becomes this:

cache [current_user.id, current_user.products.count, current_user.products.maximum(:updated_at)]

We use current_user.id because if you use Devise, User model is updated for each login. id attribute is less frequently changed.

There is a good chance that you use Kaminari for pagination, page information need also to be included.

cache [current_user.id, current_user.products.count, current_user.products.maximum(:updated_at), params[:page]]

Finally, you might have query in index method for searching. Depending on your route, you might not want to cache collection is there is a query in the route. Use cache_if for that:

cache_if params[:q].blank?, [current_user.id, current_user.products.count, current_user.products.maximum(:updated_at), params[:page]]

That is a pretty long cache key. Fortunately, only two SQL queries are used (count and maximum).

Simple Recurrence Rule for IceCube

  • Mar 17

When it comes to recurrence event for Rails, IceCube is probably the first choice. While it is very good at calculating recurrence, saving recurrence rule is not trivial. And a UI for complicated recurrences is difficult. In any case, this is a simplified recurrence rule for IceCube. It might help in some way.

IceCube models recurrence based on rrule of iCal. Therefore, these are the basic rules:

FREQ: D(daily), W(weekly), M(monthly), Y(yearly)
COUNT: #x where x is number
INTERVAL: +x where x is interval
BY use dot (.)
  BYDAY: .SU, .MO, .TU, .WE, .TH, .FR, .SA, .1FR, .-1FR, .20MO
  BYMONTHDAY: .D1-31 (+/-)
  BYMONTH: .M1-12 (+/-)
  BYYEARDAY: .Y1, Y100, Y200
  BYWEEKNO: .W20 (week number)
UNTIL: <YYYYMMDD

Week starts at 0 (Sunday), month and year at 1.

As a result, daily for 10 occurrences look like this:

RRULE:FREQ=DAILY;COUNT=10
D#10

And 'Every Friday the 13th' is

RRULE:FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13
M.FR.D13

Here is the code for conversion between this simplified rule and IceCube. Many more examples and test are included.

Such simplified rules can be saved in text field in database and used in HTML select options. Multiple recurrence rules can also be put together in one sentence separated by space.

Hope this approach can ease the problem of using IceCube in Rails application.

Rails 4.2 Tutorial 4 - Tagging with PostgreSQL array

  • Mar 9

Tagging is a very common practice in web application. There are many good gems to begin with. But each application uses tags differently and it is quite easy to make one which fits your application best. Here, we will also use PostgreSQL array to implement tags. Actually, there is an existing gem for that: acts-as-taggable-array-on. But the scenario is slightly different in our case.

First, we need a model to be tagged. Since we have users already, we will create model Topic and users can have many topics and topics might belongs to many users with different permission.

$ bin/rails g scaffold Topic title:text description:text public:boolean
$ bin/rails g model Ownership user:references topic:references permission:integer

The resulted migration are slightly edited:

class CreateTopics < ActiveRecord::Migration
  def change
    create_table :topics do |t|
      t.text :title,         null: false
      t.text :description
      t.boolean :public, default: false

      t.timestamps           null: false
    end
  end
end
class CreateOwnerships < ActiveRecord::Migration
  def change
    create_table :ownerships do |t|
      t.references :user, index: true
      t.references :topic, index: true
      t.integer :permission, default: 1

      t.timestamps null: false
    end
    add_foreign_key :ownerships, :users, on_delete: :cascade
    add_foreign_key :ownerships, :topics, on_delete: :cascade
  end
end

You can use null: false to prevent a field from having null value; default: 1 for default value and on_delete: :cascade for foreign key to keep database integrity.

Let's look at the relationship between user and topic first:

# app/model/user.rb
class User < ActiveRecord::Base
  include OmniauthCallbacks

  has_many :ownerships
  has_many :topics, :through => :ownerships

  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :validatable, :recoverable and :registerable
  devise :database_authenticatable, :omniauthable, :rememberable, :trackable

end
# app/models/topic.rb 
class Topic < ActiveRecord::Base
  has_many :ownerships
  has_many :users, through: :ownerships
end
# app/models/ownership.rb 
class Ownership < ActiveRecord::Base
  belongs_to :user
  belongs_to :topic

  enum permission: {owner: 1, subscriber: 4}
end

This is a typic many-to-many relationship. Note that the use of ActiveRecord::Enum on permission. It is an easy way to support enum attribute.

Run bin/rake db:migrate to create database.

Now, we can start to test these relationships to make sure they are properly connected.

# test/fixtures/topics.yml
topic_one:
  title: Topic One Title
  description: Topic One Description
  public: false

topic_two:
  title: Topic Two Title
  description: Topic Two Description
  public: false

topic_three:
  title: Topic Three Title
  description: Topic Three Description
  public: false
# test/fixtures/ownerships.yml
ownership_one:
  user: user_one
  topic: topic_one
  permission: 1

ownership_two:
  user: user_two
  topic: topic_two
  permission: 1

ownership_three:
  user: user_two
  topic: topic_three
  permission: 1

Then we can test like this:

# test/models/user_test.rb 
require 'test_helper'

class UserTest < ActiveSupport::TestCase
  test "User one has one topic" do
    user = users(:user_one)
    assert_equal user.topics.count, 1
  end

  test "User two has two topic" do
    user = users(:user_two)
    assert_equal user.topics.count, 2
  end
end
# test/models/topic_test.rb 
require 'test_helper'

class TopicTest < ActiveSupport::TestCase
  test "Topic one has one hyperlink" do
    topic = topics(:topic_one)
    assert_equal topic.hyperlinks.count, 1
  end                                                                                                         
                                                                                                              
  test "Topic three has two hyperlinks" do
    topic = topics(:topic_three)
    assert_equal topic.hyperlinks.count, 2
  end   

  test "User two is the owner of topic two" do
    user = users(:user_two)
    topic = topics(:topic_two)
    assert_includes topic.users, user
  end   

  test "User one is not the owner of topic two" do
    user = users(:user_one)
    topic = topics(:topic_two)
    assert_not_includes topic.users, user
  end   
end

As for the default controller test, remember to sign-in since all actions need user sign-in:

# test/controllers/topics_controller_test.rb

class TopicsControllerTest < ActionController::TestCase
  setup do
    @topic = topics(:topic_one)
    @user = users(:user_one)
    sign_in @user
  end

If you have problem about foreign key constraint, your user role in PostgreSQL might need to be supervisor. Run psql and execute:

ALTER ROLE bookmark SUPERVISOR

Since a topic can belong to differnet users, each user might tag a topic differently. Therefore, we cannot put the tags on topics. Instead, we will store the tags in ownerships. To add PostgreSQL to model, try this:

$ bin/rails g migration AddTagsToOwnership tags:text

And the migration file:

class AddTagsToOwnership < ActiveRecord::Migration
  def change
    add_column :ownerships, :tags, :text, array: true
    add_index :ownerships, :tags, using: 'gin'
  end
end

By assigning arrays: true, it turns the attribute tags into PostgreSQL array.

Then you can use the fields like this:

ownership = Ownership.first
ownership.update_attribute(:tags, ['red', 'white'])

ownership.tags << 'blue'
ownership.save!

Rails does not track the changes of PostgreSQL array perfectly. It might be wise to notify the change of attribute:

ownership.tags.delete('red')
ownership.tags_will_change!
ownership.save!

The use of #{attribute_name}_will_change! is the key.

Then we can copy what acts-as-taggable-array-on does, but apply it on Ownership model. We put the code in Concern like this:

# app/models/concerns/taggable.rb 
module TagParser  
  class Parser
    def parse tags
      case tags
      when String
        tags.split(/[ ]*,[ ]*/)
      else
        tags
      end
    end
  end
end

module Concerns::Taggable
  extend ActiveSupport::Concern

  included do
#    taggable_on :folders
    taggable_on :tags
  end

  module ClassMethods
    def taggable_on(field_name)
      tag_names = field_name.to_s.gsub(/[^A-Za-z]/, '') # Sanitize
      parser = TagParser::Parser.new
      tag_name = tag_names.singularize

      Topic.scope :"with_any_#{tag_name}", ->(tags){ joins(:ownerships).where("ownerships.#{tag_names} && ARRAY[?]", parser.parse(
tags)) }
      Topic.scope :"with_all_#{tag_names}", ->(tags){ joins(:ownerships).where("ownerships.#{tag_names} @> ARRAY[?]", parser.parse
(tags)) }
      Topic.scope :"without_any_#{tag_name}", ->(tags){ joins(:ownerships).where.not("ownerships.#{tag_names} && ARRAY[?]", parser
.parse(tags)) }
      Topic.scope :"without_all_#{tag_names}", ->(tags){ joins(:ownerships).where.not("ownerships.#{tag_names} @> ARRAY[?]", parse
r.parse(tags)) }
      Topic.scope :"all_#{tag_names}", -> { joins(:ownerships).select("unnest('ownerships.#{tag_names})").uniq.pluck(tag_names).co
mpact }
      class_eval %Q{
        def #{tag_names}_by(user:)
          o = Ownership.find_by(user: user, topic: self)
          o.present? ? o.#{tag_names} : nil
        end
      }
    end
  end
end

Then include it in Topic:

class Topic < ActiveRecord::Base
  has_many :ownerships
  has_many :users, through: :ownerships

  include Concerns::Taggable
end

Finally, a few test to make sure it works:

# test/models/tag_test.rb 
require 'test_helper'

class TopicTest < ActiveSupport::TestCase
  test "Topic has tags" do
    user = users(:user_one)
    topic = topics(:topic_one)
    assert topic.tags_by(user: user).blank?
  end                                                                                                         

  test "Topic can set tags" do
    user = users(:user_one)
    topic = topics(:topic_one)
    ownership = Ownership.find_by(user: user, topic: topic)
    ownership.tags = ['summer']
    ownership.save!
    assert topic.tags_by(user: user).include?('summer')
  end                                                                                                         

  test "Topic can add tags" do
    user = users(:user_one)
    topic = topics(:topic_one)
    ownership = Ownership.find_by(user: user, topic: topic)
    ownership.tags = ['summer']
    ownership.save!
    ownership.tags << 'swim'
    ownership.save!
    assert topic.tags_by(user: user).include?('swim')
  end                                                                                                         

  test "Topic can delete tags" do
    user = users(:user_one)
    topic = topics(:topic_one)
    ownership = Ownership.find_by(user: user, topic: topic)
    ownership.tags = ['summer', 'swim']
    ownership.save!
    assert topic.tags_by(user: user).include?('swim')
    ownership.tags.delete 'swim'
    ownership.tags_will_change!
    ownership.save!
    assert_not topic.tags_by(user: user).include?('swim')
  end                                                                                                         

  test "Find topic with tag" do
    user = users(:user_one)
    topic_one = topics(:topic_one)
    ownership = Ownership.find_by(user: user, topic: topic_one)
    ownership.update_attribute(:tags, ['summer', 'swim'])
    assert topic_one.tags_by(user: user).include?('swim')
    assert_equal topic_one, user.topics.with_any_tag('summer').first
  end                                                                                                         

  test "Find topic with any tag" do
    user_one = users(:user_one)
    user_two = users(:user_two)
    topic_one = topics(:topic_one)
    topic_two = topics(:topic_two)
    Ownership.find_by(user: user_one, topic: topic_one).update_attribute(:tags, ['summer', 'swim'])
    Ownership.find_by(user: user_two, topic: topic_two).update_attribute(:tags, ['winter', 'swim'])
    assert_equal 1, user_one.topics.with_any_tag('summer, winter, spring, fall').count
    assert_equal 2, Topic.with_any_tag('summer, winter, spring, fall').count
  end                                                                                                         

  test "Find topic with all tags" do
    user_one = users(:user_one)
    user_two = users(:user_two)
    topic_one = topics(:topic_one)
    topic_two = topics(:topic_two)
    Ownership.find_by(user: user_one, topic: topic_one).update_attribute(:tags, ['summer', 'swim'])
    assert user_one.topics.with_all_tags('summer, winter, spring, fall').blank?
    assert_equal 1, user_one.topics.with_all_tags('summer, swim').count
  end                                                                                                         
end

Rails, Shippable and PostgreSQL

  • Mar 5

I code on cloud most of time using Nitrous.io and use Bitbucket as git repositories. The Rails application is deployed to Heroku. You can connect to Nitrous via SSH through Secure Shell and it feels like a regular Unix environment. Bitbucket is of good quality as Github and allows private repostories, which is important for some personal projects. Free service is not without limit. I have limited memory and disk space on Nitrous.io, even though it is already more generous than other cloud development environment and you can use your N2O to trade disk space with memory. To make test more efficient and take less resource, I decide to use Shippable for regular testing.

It is related easy to set it up. First, sign up with your bitbucket account. Then you can choice the project for shippable. To begin with, you need a shippable.yml at root environment like this:

language: ruby
rvm:
  - 2.1.2
script:
  - bundle exec rake test

It is a bare minimum setup. If you need to do something before the Rails is installed, you can use before_install section in shippable.yml.

Once you commit new code to bitbucket, shippable will start to run and you can fix the problem one-by-one at shippable console. It is very straight forward and easy.

I use Figaro for secret token and the application.yml is not in the git repository, shippable will complain. Since I currently only use shippable for test, I decode to hard-code the secret for test environment like this:

# config/secrets.yml 
development:
  secret_key_base: <%= ENV["RAILS_SECRET_KEY"] %>

test:
  secret_key_base: 'Put our secret token for test environment here.'

production:
  secret_key_base: <%= ENV["RAILS_SECRET_KEY"] %>

And in devise initializer

# config/initializers/devise.rb
Devise.setup do |config|
  if Rails.env.test?
    config.secret_key = 'Put our devise token for test environment here.'
  else
    config.secret_key = ENV['DEVISE_SECRET_TOKEN']
  end
  ...

You can always use rake secret to generate new token.

If you have problem related to PostgreSQL:

PG::ConnectionBad: FATAL:  Peer authentication failed for user "xxx"

You can fix it based on this article

#config/database.shippable.yml 
development:
  adapter: postgresql
  encoding: unicode
  database: app_development
  pool: 5
  username: postgres
  password:

test:
  adapter: postgresql
  encoding: unicode
  database: app_test
  pool: 5
  username: postgres
  password:
  min_messages: warning

production:
  adapter: postgresql
  encoding: unicode
  database: app_production
  pool: 5
  username: postgres
  password:

Here is the final shippable.yml:

language: ruby
rvm:
  - 2.1.2
before_script:
  - cp config/database.shippable.yml config/database.yml
  - bundle exec rake db:setup
script:
  - bundle exec rake test

Note the use of db:setup to set up database is better than migration.

Now, it should runs as what you have in a console.

Shippable supports test visualization in JUnit format. To use it, you need minitest-reporters:

# Gemfile

group :development, :test do
  gem 'minitest-reporters'
end
# test/test_helper.rb 
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
require 'minitest/reporters'

MiniTest::Reporters.use!([
  MiniTest::Reporters::DefaultReporter.new,
  MiniTest::Reporters::JUnitReporter.new(
    ENV["CI_REPORTS"] || "log/ci"
  )
])
...
# shippable.yml 
language: ruby
rvm:
  - 2.1.2
before_install:
  - sed '/ruby ENV/d' Gemfile > Gemfile.out; mv Gemfile.out Gemfile;
before_script:
  - cp config/database.shippable.yml config/database.yml
  - bundle exec rake db:setup
script:
  - bundle exec rake test
env:
  - CI_REPORTS=shippable/testresults

I put local JUnit result under log to avoid being tracked by git and I generally don't read JUnit result on console. Now, it should work as expected.