Skip to content

Commit 9c16d4c

Browse files
Merge pull request #22 from deployhq/add-test-infrastructure
feat: Add test infrastructure with multi-version Ruby support
2 parents ff540b4 + d766068 commit 9c16d4c

File tree

13 files changed

+586
-15
lines changed

13 files changed

+586
-15
lines changed

.github/workflows/ci.yml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@ name: CI
22
on: [push]
33

44
jobs:
5-
lint:
5+
test:
66
runs-on: ubuntu-latest
7+
strategy:
8+
matrix:
9+
ruby-version: ['2.7', '3.0', '3.1', '3.2', '3.3', '3.4']
710
steps:
811
- uses: actions/checkout@v4
912
- uses: ruby/setup-ruby@v1
1013
with:
11-
ruby-version: 3.4.1
14+
ruby-version: ${{ matrix.ruby-version }}
1215
- name: Install dependencies
1316
run: bundle install
14-
- name: Run linter
15-
run: bundle exec rubocop
17+
- name: Run linter and tests
18+
run: bundle exec rake
1619

1720
release-please:
1821
runs-on: ubuntu-latest
@@ -26,7 +29,7 @@ jobs:
2629

2730
release:
2831
runs-on: ubuntu-latest
29-
needs: [lint, release-please]
32+
needs: [test, release-please]
3033
if: ${{ needs.release-please.outputs.release_created }}
3134
steps:
3235
- uses: actions/checkout@v4

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ examples
44
.idea
55
Deployfile
66
Gemfile.lock
7+
spec/examples.txt

.rspec

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
--require spec_helper
2+
--format documentation
3+
--color

.rubocop.yml

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
11
AllCops:
22
TargetRubyVersion: 2.7
33
NewCops: enable
4+
SuggestExtensions: false
45

56
Metrics/BlockLength:
67
Exclude:
78
- spec/**/*
9+
- test/**/*
810

911
Metrics/ModuleLength:
1012
Exclude:
1113
- spec/**/*
14+
- test/**/*
15+
16+
Metrics/MethodLength:
17+
Max: 14
18+
Exclude:
19+
- test/**/*
20+
21+
Metrics/AbcSize:
22+
Exclude:
23+
- test/**/*
1224

1325
Style/StringConcatenation:
1426
Enabled: false
@@ -20,8 +32,9 @@ Naming/FileName:
2032
Exclude:
2133
- lib/klogger-logger.rb
2234

23-
Metrics/MethodLength:
24-
Max: 14
35+
Naming/PredicateMethod:
36+
Exclude:
37+
- lib/deploy/resource.rb
2538

2639
Metrics/ClassLength:
2740
Max: 120

CLAUDE.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
DeployHQ Ruby API library and CLI client. Provides programmatic access to the DeployHQ deployment platform and a command-line tool for triggering deployments.
8+
9+
## Development Commands
10+
11+
### Setup
12+
```bash
13+
bundle install
14+
```
15+
16+
### Linting and testing
17+
```bash
18+
# Run all checks (linting + tests)
19+
bundle exec rake
20+
21+
# Run only linting
22+
bundle exec rubocop
23+
24+
# Run only tests
25+
bundle exec rspec
26+
27+
# Run a specific test file
28+
bundle exec rspec spec/configuration_spec.rb
29+
30+
# Run tests with verbose output
31+
bundle exec rspec --format documentation
32+
```
33+
34+
### Building the gem
35+
```bash
36+
gem build deployhq.gemspec
37+
```
38+
39+
### Testing CLI locally
40+
```bash
41+
ruby -Ilib bin/deployhq <command>
42+
```
43+
44+
## Architecture
45+
46+
### Core Components
47+
48+
**Deploy Module** (`lib/deploy.rb`): Main entry point that provides configuration management via `Deploy.configure` and `Deploy.configuration`. Configuration can be loaded from files using `Deploy.configuration_file=`.
49+
50+
**Resource System**: Base class pattern where `Deploy::Resource` provides ActiveRecord-like interface for API objects:
51+
- `find(:all)` and `find(id)` for retrieval
52+
- `save`, `create`, `update` for persistence
53+
- `destroy` for deletion
54+
- Child resources (Project, Deployment, Server, ServerGroup, DeploymentStep, DeploymentStepLog) inherit this behavior
55+
56+
**Request Layer** (`lib/deploy/request.rb`): HTTP client using Net::HTTP with basic auth. Handles JSON serialization/deserialization and translates HTTP status codes to appropriate exceptions or boolean success states.
57+
58+
**CLI** (`lib/deploy/cli.rb`): OptionParser-based command-line interface with three main commands:
59+
- `configure`: Interactive setup wizard for creating Deployfile
60+
- `servers`: Lists servers and server groups
61+
- `deploy`: Interactive deployment workflow with real-time progress via WebSocket
62+
63+
**Configuration** (`lib/deploy/configuration.rb`): Loads from JSON Deployfile containing:
64+
- `account`: DeployHQ account URL (e.g., https://account.deployhq.com)
65+
- `username`: User email or username
66+
- `api_key`: API key from user profile
67+
- `project`: Default project permalink
68+
- `websocket_hostname`: Optional WebSocket endpoint (defaults to wss://websocket.deployhq.com)
69+
70+
### Resource Relationships
71+
72+
Projects contain Servers and ServerGroups. Deployments belong to Projects and have DeploymentSteps. DeploymentSteps have DeploymentStepLogs. All child resources use the `:project` param to construct proper API paths like `projects/:permalink/deployments/:id`.
73+
74+
### WebSocket Integration
75+
76+
`Deploy::CLI::WebSocketClient` connects to deployment progress streams. `Deploy::CLI::DeploymentProgressOutput` consumes WebSocket messages and renders deployment progress to terminal in real-time.
77+
78+
## Release Process
79+
80+
This project uses [Conventional Commits](https://www.conventionalcommits.org/) for automated releases via [release-please](https://github.com/googleapis/release-please).
81+
82+
**Commit Message Format**:
83+
- `feat:` or `feature:` - New features
84+
- `fix:` - Bug fixes
85+
- `docs:` - Documentation changes
86+
- `refactor:` - Code refactoring
87+
- `perf:` - Performance improvements
88+
- `chore:` - Maintenance tasks
89+
90+
On merge to master, release-please analyzes commits and creates a release PR. When merged, it:
91+
1. Updates CHANGELOG.md
92+
2. Bumps version in lib/deploy/version.rb
93+
3. Creates GitHub release
94+
4. Publishes gem to RubyGems
95+
96+
## Configuration File
97+
98+
Each developer needs a `Deployfile` in their working directory (not committed). Use `deployhq configure` to generate interactively, or create manually following `Deployfile.example`.

Gemfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,10 @@ source 'http://rubygems.org'
55

66
gemspec
77

8+
gem 'rake', '~> 13.0'
89
gem 'rubocop'
10+
11+
group :test do
12+
gem 'rspec', '~> 3.13'
13+
gem 'webmock', '~> 3.23'
14+
end

Rakefile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# frozen_string_literal: true
2+
3+
require 'rspec/core/rake_task'
4+
require 'rubocop/rake_task'
5+
6+
RSpec::Core::RakeTask.new(:spec)
7+
8+
RuboCop::RakeTask.new
9+
10+
task default: [:rubocop, :spec]

deployhq.gemspec

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Gem::Specification.new do |s|
1919
s.add_dependency('json', '~> 2.6')
2020
s.add_dependency('websocket-eventmachine-client', '~> 1.2')
2121

22-
s.authors = ['Adam Cooke']
23-
s.email = ['[email protected]']
24-
s.homepage = 'https://github.com/krystal/deployhq-lib'
22+
s.authors = ['DeployHQ Team']
23+
s.email = ['[email protected]']
24+
s.homepage = 'https://github.com/deployhq/deployhq-lib'
2525
end

lib/deploy/cli.rb

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,17 +100,17 @@ def invoke(args)
100100

101101
def server_list
102102
@server_groups ||= @project.server_groups
103-
if @server_groups.count.positive?
103+
if @server_groups.any?
104104
@server_groups.each do |group|
105105
puts "Group: #{group.name}"
106106
puts group.servers.map { |server| format_server(server) }.join("\n\n")
107107
end
108108
end
109109

110110
@ungrouped_servers ||= @project.servers
111-
return unless @ungrouped_servers.count.positive?
111+
return unless @ungrouped_servers.any?
112112

113-
puts "\n" if @server_groups.count.positive?
113+
puts "\n" if @server_groups.any?
114114
puts 'Ungrouped Servers'
115115
puts @ungrouped_servers.map { |server| format_server(server) }.join("\n\n")
116116
end
@@ -197,15 +197,13 @@ def format_server(server)
197197
end.join("\n")
198198
end
199199

200-
# rubocop:disable Lint/FormatParameterMismatch
201200
def format_kv_pair(hash)
202201
longest_key = hash.keys.map(&:length).max + 2
203202
hash.each_with_index.map do |(k, v), _i|
204203
str = format("%#{longest_key}s : %s", k, v)
205204
str
206205
end.join("\n")
207206
end
208-
# rubocop:enable Lint/FormatParameterMismatch
209207

210208
end
211209

spec/configuration_spec.rb

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
require 'tempfile'
5+
6+
RSpec.describe Deploy::Configuration do
7+
describe '#websocket_hostname' do
8+
it 'has a default value' do
9+
config = described_class.new
10+
expect(config.websocket_hostname).to eq('wss://websocket.deployhq.com')
11+
end
12+
13+
it 'allows setting a custom value' do
14+
config = described_class.new
15+
config.websocket_hostname = 'wss://custom.example.com'
16+
expect(config.websocket_hostname).to eq('wss://custom.example.com')
17+
end
18+
end
19+
20+
describe 'attribute setters and getters' do
21+
it 'sets and retrieves all configuration attributes' do
22+
config = described_class.new
23+
config.account = 'https://test.deployhq.com'
24+
config.username = 'testuser'
25+
config.api_key = 'test-api-key'
26+
config.project = 'test-project'
27+
28+
expect(config.account).to eq('https://test.deployhq.com')
29+
expect(config.username).to eq('testuser')
30+
expect(config.api_key).to eq('test-api-key')
31+
expect(config.project).to eq('test-project')
32+
end
33+
end
34+
35+
describe '.from_file' do
36+
context 'with all fields present' do
37+
it 'loads configuration from JSON file' do
38+
config_data = {
39+
'account' => 'https://test.deployhq.com',
40+
'username' => 'testuser',
41+
'api_key' => 'test-key',
42+
'project' => 'test-project',
43+
'websocket_hostname' => 'wss://test.example.com'
44+
}
45+
46+
Tempfile.create(['config', '.json']) do |f|
47+
f.write(JSON.generate(config_data))
48+
f.flush
49+
f.rewind
50+
51+
config = described_class.from_file(f.path)
52+
53+
expect(config.account).to eq('https://test.deployhq.com')
54+
expect(config.username).to eq('testuser')
55+
expect(config.api_key).to eq('test-key')
56+
expect(config.project).to eq('test-project')
57+
expect(config.websocket_hostname).to eq('wss://test.example.com')
58+
end
59+
end
60+
end
61+
62+
context 'without websocket_hostname' do
63+
it 'uses the default websocket hostname' do
64+
config_data = {
65+
'account' => 'https://test.deployhq.com',
66+
'username' => 'testuser',
67+
'api_key' => 'test-key',
68+
'project' => 'test-project'
69+
}
70+
71+
Tempfile.create(['config', '.json']) do |f|
72+
f.write(JSON.generate(config_data))
73+
f.flush
74+
f.rewind
75+
76+
config = described_class.from_file(f.path)
77+
78+
expect(config.account).to eq('https://test.deployhq.com')
79+
expect(config.websocket_hostname).to eq('wss://websocket.deployhq.com')
80+
end
81+
end
82+
end
83+
84+
context 'with missing file' do
85+
it 'raises Errno::ENOENT' do
86+
expect do
87+
described_class.from_file('/nonexistent/file.json')
88+
end.to raise_error(Errno::ENOENT)
89+
end
90+
end
91+
92+
context 'with invalid JSON' do
93+
it 'raises JSON::ParserError' do
94+
Tempfile.create(['config', '.json']) do |f|
95+
f.write('invalid json {')
96+
f.flush
97+
f.rewind
98+
99+
expect do
100+
described_class.from_file(f.path)
101+
end.to raise_error(JSON::ParserError)
102+
end
103+
end
104+
end
105+
end
106+
end

0 commit comments

Comments
 (0)