Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2026-10-25 15:08:27 UTC using RuboCop version 1.92.1.
# on 2026-10-25 21:03:15 UTC using RuboCop version 1.92.1.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
Expand Down Expand Up @@ -72,7 +72,7 @@ RSpec/ContextWording:
- 'spec/models/team_spec.rb'
- 'spec/support/api/endpoints/it_behaves_like_a_cursor_api.rb'

# Offense count: 13
# Offense count: 15
# Configuration parameters: CountAsOne.
RSpec/ExampleLength:
Max: 11
Expand All @@ -94,7 +94,7 @@ RSpec/LetSetup:
RSpec/MessageSpies:
EnforcedStyle: receive

# Offense count: 29
# Offense count: 35
RSpec/MultipleExpectations:
Max: 6

Expand Down Expand Up @@ -128,7 +128,7 @@ RSpec/SpecFilePathFormat:
- 'spec/commands/whoami_spec.rb'
- 'spec/version_spec.rb'

# Offense count: 11
# Offense count: 13
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
# URISchemes: http, https
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
### Changelog

* 2026/10/25: [#88](https://github.com/slack-ruby/slack-shellbot/pull/88): Upgrade slack-ruby-bot-server to 2.3.0 and rack-server-pages to 0.3.0, version website assets by content, and configure template revalidation centrally to prevent stale browser assets after deployments - [@dblock](https://github.com/dblock), [@Copilot](https://github.com/apps/copilot-swe-agent).
* 2026/10/25: [#87](https://github.com/slack-ruby/slack-shellbot/pull/87): Redesign the Shell website with shared responsive layouts, charcoal ribbon artwork, a black-bordered animated demo, an "Is this a joke?" FAQ linked from the homepage, and recommendations with original logos for Slava, Strada, Gamebot, and S'Up above Open source while preserving Slack installation and status flows - [@dblock](https://github.com/dblock), [@Copilot](https://github.com/apps/copilot-swe-agent).
* 2026/10/24: [#86](https://github.com/slack-ruby/slack-shellbot/pull/86): Upgrade slack-ruby-bot-server to 2.2.1 to fix Grape error handling for unsupported requests and failing health checks - [@dblock](https://github.com/dblock), [@Copilot](https://github.com/apps/copilot-swe-agent).
* 2026/10/23: Upgrade to Ruby 4.0.9 and refresh dependencies and CI browser tooling - [@dblock](https://github.com/dblock), [@Copilot](https://github.com/apps/copilot-swe-agent).
Expand Down
3 changes: 2 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ gem 'mongoid-scroll'
gem 'newrelic_rpm'
gem 'puma'
gem 'rack-robotz'
gem 'rack-server-pages'
gem 'rack-server-pages', '~> 0.3.0'
gem 'slack-ruby-bot-server', '~> 2.3.0'
gem 'slack-ruby-bot-server-events'
gem 'slack-ruby-bot-server-mailchimp'

Expand Down
7 changes: 4 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ GEM
rack-rewrite (1.5.1)
rack-robotz (0.0.4)
rack
rack-server-pages (0.2.1)
rack-server-pages (0.3.0)
rack
rack-test (2.3.0)
rack (>= 1.3)
Expand Down Expand Up @@ -282,7 +282,7 @@ GEM
logger (~> 1.4)
rubyzip (>= 1.2.2, < 4.0)
websocket (~> 1.0)
slack-ruby-bot-server (2.2.1)
slack-ruby-bot-server (2.3.0)
async
foreman
grape
Expand Down Expand Up @@ -345,7 +345,7 @@ DEPENDENCIES
newrelic_rpm
puma
rack-robotz
rack-server-pages
rack-server-pages (~> 0.3.0)
rack-test
rackup
rake
Expand All @@ -355,6 +355,7 @@ DEPENDENCIES
rubocop-rake
rubocop-rspec
selenium-webdriver
slack-ruby-bot-server (~> 2.3.0)
slack-ruby-bot-server-events
slack-ruby-bot-server-mailchimp
vcr
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ The website follows the shared Slava, Strata, and Strada layout with a terminal-

The bot recommendations use local copies of the original logos: `bot-slava.png` and `bot-strada.png` from their respective projects' `public/img/icon.png`, `bot-gamebot.png` from slack-gamebot2's `public/img/game.png`, and `bot-sup.png` from slack-sup2's `public/img/icon.png`. Preserve their original colors rather than applying the Shell website's monochrome styling.

## Website assets and caching

The website uses `rack-server-pages` 0.3.0's `asset_path` helper for content-versioned CSS, JavaScript, images, and favicon URLs. Use the helper for new local asset references rather than fixed URLs or manual version strings. `public/styles.css.erb` is served at `/styles.css` and versions the background image reference too.

The `config.server_pages` block in `config/initializers/slack_ruby_bot_server.rb`, supported by `slack-ruby-bot-server` 2.3.0, sets `Cache-Control: private, no-cache` for templates, including HTML and the generated stylesheet, requiring revalidation before reuse. This lets the stylesheet pick up a changed background image even when its own source is unchanged. Static files use the library's default content-based `ETag` and timestamp validation. Versioned URLs serve current files rather than archived versions; do not add `immutable` or long freshness lifetimes. Existing cached pages need to fetch fresh HTML before they can use the new URLs.

## Copyright and License

Copyright (c) 2016-2025, Daniel Doubrovkine, Artsy and [Contributors](CHANGELOG.md).
Expand Down
3 changes: 3 additions & 0 deletions config/initializers/slack_ruby_bot_server.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
SlackRubyBotServer.configure do |config|
config.oauth_version = :v2
config.oauth_scope = ['chat:write', 'im:history', 'mpim:history', 'channels:history', 'groups:history']
config.server_pages do |pages|
pages.cache_control = 'private, no-cache'
end
end
16 changes: 8 additions & 8 deletions public/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
<head>
<title>Shell: A Unix Shell in Every Slack Channel</title>
<%= partial 'public/partials/_head.html.erb' %>
<script src="/scripts/register.js"></script>
<script src="/scripts/status.js"></script>
<script src="<%= h asset_path('/scripts/register.js') %>"></script>
<script src="<%= h asset_path('/scripts/status.js') %>"></script>
<meta name="slack-app-id" content="A01F73M28BF">
<meta name="description" content="Add a rudimentary Unix shell to every Slack channel.">
</head>
Expand All @@ -13,7 +13,7 @@
<main id="main">
<section class="hero page-container" aria-labelledby="hero-title">
<div class="hero-intro">
<img class="hero-logo" src="/img/icon.png" width="512" height="512" alt="Shell terminal logo">
<img class="hero-logo" src="<%= h asset_path('/img/icon.png') %>" width="512" height="512" alt="Shell terminal logo">
<div>
<h1 id="hero-title">Shell for Slack</h1>
<p class="hero-tagline">Whoa, a bash shell inside Slack!</p>
Expand All @@ -28,7 +28,7 @@
</div>
</div>
<figure class="hero-preview">
<img src="/img/shell.gif" width="691" height="419" alt="Shell running Unix commands inside a Slack channel">
<img src="<%= h asset_path('/img/shell.gif') %>" width="691" height="419" alt="Shell running Unix commands inside a Slack channel">
<figcaption>A little command line, right where your team works.</figcaption>
</figure>
</section>
Expand All @@ -47,25 +47,25 @@
<p class="section-intro">A little more play, a little more connection.</p>
<ul class="bot-cards">
<li class="bot-card">
<img class="bot-logo" src="/img/bot-slava.png" width="72" height="72" alt="" loading="lazy">
<img class="bot-logo" src="<%= h asset_path('/img/bot-slava.png') %>" width="72" height="72" alt="" loading="lazy">
<h3>Strava in Slack</h3>
<p>Share Strava activities, celebrate every finish, and keep your Slack team moving.</p>
<a class="secondary-button" href="https://slava.playplay.io/">Meet Slava</a>
</li>
<li class="bot-card">
<img class="bot-logo" src="/img/bot-strada.png" width="72" height="72" alt="" loading="lazy">
<img class="bot-logo" src="<%= h asset_path('/img/bot-strada.png') %>" width="72" height="72" alt="" loading="lazy">
<h3>Strava in Discord</h3>
<p>Bring your running or cycling club together with Strava activities in Discord.</p>
<a class="secondary-button" href="https://strada.playplay.io/">Meet Strada</a>
</li>
<li class="bot-card">
<img class="bot-logo" src="/img/bot-sup.png" width="72" height="72" alt="" loading="lazy">
<img class="bot-logo" src="<%= h asset_path('/img/bot-sup.png') %>" width="72" height="72" alt="" loading="lazy">
<h3>S'Up for Slack</h3>
<p>Meet someone new over coffee with fresh groups of three from your Slack channel each week.</p>
<a class="secondary-button" href="https://sup2.playplay.io/">Meet S'Up</a>
</li>
<li class="bot-card">
<img class="bot-logo" src="/img/bot-gamebot.png" width="72" height="72" alt="" loading="lazy">
<img class="bot-logo" src="<%= h asset_path('/img/bot-gamebot.png') %>" width="72" height="72" alt="" loading="lazy">
<h3>Leaderboards in Slack</h3>
<p>Challenge teammates, report scores, and climb your Slack channel's leaderboard.</p>
<a class="secondary-button" href="https://gamebot2.playplay.io/">Meet Gamebot</a>
Expand Down
10 changes: 5 additions & 5 deletions public/partials/_head.html.erb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="/favicon.ico?v=charcoal" type="image/x-icon">
<script src="/scripts/jquery-1.7.1.min.js"></script>
<script src="/scripts/url.min.js"></script>
<script src="/scripts/shell.js"></script>
<link rel="stylesheet" type="text/css" href="/styles.css">
<link rel="icon" href="<%= h asset_path('/favicon.ico') %>" type="image/x-icon">
<script src="<%= h asset_path('/scripts/jquery-1.7.1.min.js') %>"></script>
<script src="<%= h asset_path('/scripts/url.min.js') %>"></script>
<script src="<%= h asset_path('/scripts/shell.js') %>"></script>
<link rel="stylesheet" type="text/css" href="<%= h asset_path('/styles.css') %>">
2 changes: 1 addition & 1 deletion public/styles.css → public/styles.css.erb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ body {
z-index: -1;
content: "";
pointer-events: none;
background: linear-gradient(#ffffff66, #ffffff66), url("/img/screenshot-background.svg") center top / 100% 100% no-repeat;
background: linear-gradient(#ffffff66, #ffffff66), url("<%= asset_path('/img/screenshot-background.svg') %>") center top / 100% 100% no-repeat;
}

.site-page > main { flex: 1; }
Expand Down
7 changes: 7 additions & 0 deletions spec/integration/index_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@
expect(page).to have_css('#is-this-a-joke p', text: 'Yes. Also, it works!')
end

it 'loads the generated stylesheet and versioned artwork after a reload' do
page.refresh
background = page.evaluate_script("getComputedStyle(document.body, '::before').backgroundImage")
expect(background).to match(%r{/img/screenshot-background\.svg\?v=[a-f0-9]{64}})
expect(page).to have_css('.hero-preview img') { |image| image.evaluate_script('this.complete && this.naturalWidth > 0') }
end

it 'links to the bot recommendations after How to use in navigation' do
expect(page).to have_css('.nav-links a[href="/#how-to-use"] + a[href="/#more-bots"]', text: /More bots/i)
within('nav') { click_link 'More bots' }
Expand Down
56 changes: 54 additions & 2 deletions spec/slack_ruby_bot_server/api/middleware_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,52 @@
expect(JSON.parse(last_response.body)).to include('type' => 'other_error', 'message' => '405 Not Allowed')
end

describe 'website asset caching' do
['/', '/help.html', '/privacy.html'].each do |path|
it "serves #{path} with versioned local assets and a revalidating cache policy" do
get path

assets = Capybara.string(last_response.body).all('script[src^="/"], link[href^="/"], img[src^="/"]', visible: :all)
expect(assets).not_to be_empty
expect(assets.map { |asset| asset[:src] || asset[:href] }).to all(match(/\?v=[a-f0-9]{64}\z/))
expect(last_response.headers['Cache-Control']).to eq 'private, no-cache'
end
end

it 'renders a revalidating stylesheet with a versioned background image' do
get '/styles.css'

expect(last_response.content_type).to eq 'text/css; charset=utf-8'
expect(last_response.headers['Cache-Control']).to eq 'private, no-cache'
expect(last_response.body).to include("/img/screenshot-background.svg?v=#{Digest::SHA256.file('public/img/screenshot-background.svg').hexdigest}")
end

it 'revalidates static assets using their content ETag' do
get '/scripts/shell.js'
etag = last_response.headers['ETag']
expect(etag).to eq %("#{Digest::SHA256.file('public/scripts/shell.js').hexdigest}")

get '/scripts/shell.js', {}, 'HTTP_IF_NONE_MATCH' => etag
expect(last_response.status).to eq 304
expect(last_response.body).to be_empty
end

it 'returns the asset for an old ETag even when its timestamp matches' do
get '/scripts/shell.js', {}, 'HTTP_IF_NONE_MATCH' => '"old-content"',
'HTTP_IF_MODIFIED_SINCE' => File.mtime('public/scripts/shell.js').httpdate

expect(last_response.status).to eq 200
expect(last_response.body).to eq File.read('public/scripts/shell.js')
end
end

context 'when a health check fails' do
let(:team) { Fabricate(:team) }
let(:slack_client) { instance_double(Slack::Web::Client) }

before do
allow(Team).to receive(:asc).with(:_id).and_return([team])
allow(team).to receive(:ping!).and_raise(StandardError, 'Slack ping failed')
allow(Slack::Web::Client).to receive(:new).with(token: team.token).and_return(slack_client)
allow(slack_client).to receive(:auth_test).and_raise(StandardError, 'Slack ping failed')
end

it 'preserves the original failure in the error response' do
Expand All @@ -26,5 +66,17 @@
expect(last_response.status).to eq 400
expect(JSON.parse(last_response.body)).to include('type' => 'other_error', 'message' => 'Slack ping failed')
end

it 'deactivates revoked installations and returns an explicit failed ping' do
allow(slack_client).to receive(:auth_test).and_raise(Slack::Web::Api::Errors::SlackError, 'account_inactive')
get '/api/status'

expect(last_response.status).to eq 200
expect(JSON.parse(last_response.body)).to include(
'ping' => { 'ok' => false, 'error' => 'account_inactive' },
'active_teams_count' => 0
)
expect(team.reload.active).to be false
end
end
end
4 changes: 2 additions & 2 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
ENV['RACK_ENV'] = 'test'
Bundler.require :default, ENV['RACK_ENV']

require_relative '../app'

Dir[File.join(File.dirname(__FILE__), 'support', '**/*.rb')].each do |file|
require file
end

require_relative '../app'
Loading