crspec 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +86 -0
- data/README.md +1 -1
- data/lib/crspec/cli.rb +86 -5
- data/lib/crspec/configuration.rb +62 -9
- data/lib/crspec/dsl.rb +66 -2
- data/lib/crspec/example.rb +60 -6
- data/lib/crspec/example_group.rb +363 -63
- data/lib/crspec/execution_context.rb +10 -6
- data/lib/crspec/expectations.rb +62 -2
- data/lib/crspec/file_fixtures.rb +18 -0
- data/lib/crspec/formatters/progress_formatter.rb +37 -14
- data/lib/crspec/matchers.rb +642 -0
- data/lib/crspec/mock/argument_matchers.rb +157 -0
- data/lib/crspec/mock/double.rb +246 -13
- data/lib/crspec/mock/interceptor.rb +32 -9
- data/lib/crspec/mock/space.rb +14 -0
- data/lib/crspec/process_runner.rb +332 -0
- data/lib/crspec/rails/assets_shim.rb +33 -0
- data/lib/crspec/rails/database_isolation.rb +179 -8
- data/lib/crspec/rails/parallel.rb +29 -29
- data/lib/crspec/rails/request_helpers.rb +70 -14
- data/lib/crspec/rails/system_server.rb +7 -0
- data/lib/crspec/rails/warden_shim.rb +18 -0
- data/lib/crspec/runner.rb +143 -29
- data/lib/crspec/shared_examples.rb +70 -0
- data/lib/crspec/status_persistence.rb +46 -0
- data/lib/crspec/transpiler/cli.rb +106 -15
- data/lib/crspec/transpiler/rewriter.rb +179 -25
- data/lib/crspec/version.rb +1 -1
- data/lib/crspec.rb +6 -0
- data/lib/rspec/core.rb +2 -0
- data/lib/rspec/expectations.rb +2 -0
- data/lib/rspec/mocks.rb +2 -0
- data/lib/rspec/rails.rb +2 -0
- data/lib/rspec.rb +2 -0
- metadata +27 -156
- data/benchmark/README.md +0 -56
- data/benchmark/run.rb +0 -72
- data/benchmark/spec/user_service_crspec_spec.rb +0 -15
- data/benchmark/spec/user_service_rspec_spec.rb +0 -16
- data/benchmark/target_service.rb +0 -12
- data/benchmark/test/user_service_test.rb +0 -18
- data/demo.rb +0 -129
- data/samples/book_store/.dockerignore +0 -51
- data/samples/book_store/.gitattributes +0 -9
- data/samples/book_store/.github/dependabot.yml +0 -12
- data/samples/book_store/.github/workflows/ci.yml +0 -130
- data/samples/book_store/.gitignore +0 -35
- data/samples/book_store/.kamal/hooks/docker-setup.sample +0 -3
- data/samples/book_store/.kamal/hooks/post-app-boot.sample +0 -3
- data/samples/book_store/.kamal/hooks/post-deploy.sample +0 -14
- data/samples/book_store/.kamal/hooks/post-proxy-reboot.sample +0 -3
- data/samples/book_store/.kamal/hooks/pre-app-boot.sample +0 -3
- data/samples/book_store/.kamal/hooks/pre-build.sample +0 -51
- data/samples/book_store/.kamal/hooks/pre-connect.sample +0 -47
- data/samples/book_store/.kamal/hooks/pre-deploy.sample +0 -122
- data/samples/book_store/.kamal/hooks/pre-proxy-reboot.sample +0 -3
- data/samples/book_store/.kamal/secrets +0 -20
- data/samples/book_store/.rubocop.yml +0 -8
- data/samples/book_store/.ruby-version +0 -1
- data/samples/book_store/Dockerfile +0 -77
- data/samples/book_store/Gemfile +0 -67
- data/samples/book_store/Gemfile.lock +0 -572
- data/samples/book_store/README.md +0 -24
- data/samples/book_store/Rakefile +0 -6
- data/samples/book_store/app/assets/images/.keep +0 -0
- data/samples/book_store/app/assets/stylesheets/application.css +0 -10
- data/samples/book_store/app/controllers/application_controller.rb +0 -2
- data/samples/book_store/app/controllers/books_controller.rb +0 -30
- data/samples/book_store/app/controllers/comments_controller.rb +0 -21
- data/samples/book_store/app/controllers/concerns/.keep +0 -0
- data/samples/book_store/app/controllers/users_controller.rb +0 -21
- data/samples/book_store/app/helpers/application_helper.rb +0 -2
- data/samples/book_store/app/helpers/books_helper.rb +0 -2
- data/samples/book_store/app/helpers/comments_helper.rb +0 -2
- data/samples/book_store/app/helpers/users_helper.rb +0 -2
- data/samples/book_store/app/javascript/application.js +0 -3
- data/samples/book_store/app/javascript/controllers/application.js +0 -9
- data/samples/book_store/app/javascript/controllers/hello_controller.js +0 -7
- data/samples/book_store/app/javascript/controllers/index.js +0 -4
- data/samples/book_store/app/jobs/application_job.rb +0 -7
- data/samples/book_store/app/mailers/application_mailer.rb +0 -4
- data/samples/book_store/app/models/application_record.rb +0 -3
- data/samples/book_store/app/models/book.rb +0 -10
- data/samples/book_store/app/models/comment.rb +0 -6
- data/samples/book_store/app/models/concerns/.keep +0 -0
- data/samples/book_store/app/models/user.rb +0 -4
- data/samples/book_store/app/views/layouts/application.html.erb +0 -29
- data/samples/book_store/app/views/layouts/mailer.html.erb +0 -13
- data/samples/book_store/app/views/layouts/mailer.text.erb +0 -1
- data/samples/book_store/app/views/pwa/manifest.json.erb +0 -22
- data/samples/book_store/app/views/pwa/service-worker.js +0 -26
- data/samples/book_store/bin/brakeman +0 -7
- data/samples/book_store/bin/bundler-audit +0 -6
- data/samples/book_store/bin/ci +0 -6
- data/samples/book_store/bin/dev +0 -2
- data/samples/book_store/bin/docker-entrypoint +0 -8
- data/samples/book_store/bin/importmap +0 -4
- data/samples/book_store/bin/jobs +0 -6
- data/samples/book_store/bin/kamal +0 -16
- data/samples/book_store/bin/rails +0 -4
- data/samples/book_store/bin/rake +0 -4
- data/samples/book_store/bin/rubocop +0 -8
- data/samples/book_store/bin/setup +0 -35
- data/samples/book_store/bin/thrust +0 -5
- data/samples/book_store/config/application.rb +0 -27
- data/samples/book_store/config/boot.rb +0 -4
- data/samples/book_store/config/bundler-audit.yml +0 -5
- data/samples/book_store/config/cable.yml +0 -17
- data/samples/book_store/config/cache.yml +0 -16
- data/samples/book_store/config/ci.rb +0 -24
- data/samples/book_store/config/credentials.yml.enc +0 -1
- data/samples/book_store/config/database.yml +0 -45
- data/samples/book_store/config/deploy.yml +0 -119
- data/samples/book_store/config/environment.rb +0 -5
- data/samples/book_store/config/environments/development.rb +0 -78
- data/samples/book_store/config/environments/production.rb +0 -90
- data/samples/book_store/config/environments/test.rb +0 -53
- data/samples/book_store/config/importmap.rb +0 -7
- data/samples/book_store/config/initializers/assets.rb +0 -7
- data/samples/book_store/config/initializers/content_security_policy.rb +0 -29
- data/samples/book_store/config/initializers/filter_parameter_logging.rb +0 -8
- data/samples/book_store/config/initializers/inflections.rb +0 -16
- data/samples/book_store/config/locales/en.yml +0 -31
- data/samples/book_store/config/puma.rb +0 -42
- data/samples/book_store/config/queue.yml +0 -18
- data/samples/book_store/config/recurring.yml +0 -15
- data/samples/book_store/config/routes.rb +0 -17
- data/samples/book_store/config/storage.yml +0 -27
- data/samples/book_store/config.ru +0 -6
- data/samples/book_store/db/cable_schema.rb +0 -11
- data/samples/book_store/db/cache_schema.rb +0 -12
- data/samples/book_store/db/migrate/20260731035441_create_users.rb +0 -10
- data/samples/book_store/db/migrate/20260731035506_create_books.rb +0 -11
- data/samples/book_store/db/migrate/20260731035528_create_comments.rb +0 -11
- data/samples/book_store/db/queue_schema.rb +0 -129
- data/samples/book_store/db/schema.rb +0 -39
- data/samples/book_store/db/seeds.rb +0 -9
- data/samples/book_store/lib/tasks/.keep +0 -0
- data/samples/book_store/log/.keep +0 -0
- data/samples/book_store/public/400.html +0 -135
- data/samples/book_store/public/404.html +0 -135
- data/samples/book_store/public/406-unsupported-browser.html +0 -135
- data/samples/book_store/public/422.html +0 -135
- data/samples/book_store/public/500.html +0 -135
- data/samples/book_store/public/icon.png +0 -0
- data/samples/book_store/public/icon.svg +0 -3
- data/samples/book_store/public/robots.txt +0 -1
- data/samples/book_store/script/.keep +0 -0
- data/samples/book_store/spec/models/book_spec.rb +0 -42
- data/samples/book_store/spec/models/comment_spec.rb +0 -22
- data/samples/book_store/spec/models/user_advanced_spec.rb +0 -27
- data/samples/book_store/spec/models/user_spec.rb +0 -30
- data/samples/book_store/spec/rails_helper.rb +0 -36
- data/samples/book_store/spec/rails_parallel_spec.rb +0 -23
- data/samples/book_store/spec/requests/books_spec.rb +0 -21
- data/samples/book_store/spec/requests/users_spec.rb +0 -14
- data/samples/book_store/spec/services/inventory_service_spec.rb +0 -28
- data/samples/book_store/spec/services/time_dependent_service_spec.rb +0 -26
- data/samples/book_store/spec/spec_helper.rb +0 -16
- data/samples/book_store/spec/system/book_store_system_spec.rb +0 -18
- data/samples/book_store/storage/.keep +0 -0
- data/samples/book_store/test/controllers/.keep +0 -0
- data/samples/book_store/test/controllers/books_controller_test.rb +0 -7
- data/samples/book_store/test/controllers/comments_controller_test.rb +0 -7
- data/samples/book_store/test/controllers/users_controller_test.rb +0 -7
- data/samples/book_store/test/fixtures/books.yml +0 -11
- data/samples/book_store/test/fixtures/comments.yml +0 -11
- data/samples/book_store/test/fixtures/files/.keep +0 -0
- data/samples/book_store/test/fixtures/users.yml +0 -9
- data/samples/book_store/test/helpers/.keep +0 -0
- data/samples/book_store/test/integration/.keep +0 -0
- data/samples/book_store/test/mailers/.keep +0 -0
- data/samples/book_store/test/models/.keep +0 -0
- data/samples/book_store/test/models/book_test.rb +0 -7
- data/samples/book_store/test/models/comment_test.rb +0 -7
- data/samples/book_store/test/models/user_test.rb +0 -7
- data/samples/book_store/test/test_helper.rb +0 -15
- data/samples/book_store/tmp/.keep +0 -0
- data/samples/book_store/tmp/pids/.keep +0 -0
- data/samples/book_store/tmp/storage/.keep +0 -0
- data/samples/book_store/vendor/.keep +0 -0
- data/samples/book_store/vendor/javascript/.keep +0 -0
- data/samples/user_spec.rb +0 -31
- data/test/crspec/configuration_test.rb +0 -76
- data/test/crspec/execution_context_test.rb +0 -61
- data/test/crspec/mock_test.rb +0 -55
- data/test/crspec/rails_test.rb +0 -62
- data/test/crspec/runner_test.rb +0 -68
- data/test/crspec/transpiler_test.rb +0 -41
- data/test/test_helper.rb +0 -6
data/benchmark/target_service.rb
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "digest"
|
|
4
|
-
|
|
5
|
-
class UserService
|
|
6
|
-
def self.process_user(id, name, email)
|
|
7
|
-
# Simulate non-blocking I/O query (e.g. database lease / HTTP call)
|
|
8
|
-
sleep 0.001
|
|
9
|
-
token = Digest::SHA256.hexdigest("#{id}-#{name}-#{email}")
|
|
10
|
-
{ id: id, name: name, email: email, token: token, valid: true }
|
|
11
|
-
end
|
|
12
|
-
end
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "minitest"
|
|
4
|
-
require_relative "../target_service"
|
|
5
|
-
|
|
6
|
-
Minitest.seed ||= srand
|
|
7
|
-
|
|
8
|
-
class UserServiceTest < Minitest::Test
|
|
9
|
-
parallelize_me! if respond_to?(:parallelize_me!)
|
|
10
|
-
|
|
11
|
-
100.times do |i|
|
|
12
|
-
define_method(:"test_user_#{i}") do
|
|
13
|
-
u = UserService.process_user(i, "User #{i}", "user#{i}@example.com")
|
|
14
|
-
assert u[:valid]
|
|
15
|
-
refute_nil u[:token]
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
data/demo.rb
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "bundler/inline"
|
|
4
|
-
|
|
5
|
-
gemfile do
|
|
6
|
-
source "https://rubygems.org"
|
|
7
|
-
gem "crspec", path: __dir__
|
|
8
|
-
gem "prism"
|
|
9
|
-
gem "rails", "~> 8.0"
|
|
10
|
-
gem "sqlite3"
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
require "crspec"
|
|
14
|
-
require "rails"
|
|
15
|
-
|
|
16
|
-
puts "================================================================="
|
|
17
|
-
puts " Crspec Single-File Inline Demo & Feature Verification "
|
|
18
|
-
puts "================================================================="
|
|
19
|
-
|
|
20
|
-
# Reset global state for clean standalone run
|
|
21
|
-
Crspec.reset!
|
|
22
|
-
|
|
23
|
-
# Domain Models
|
|
24
|
-
class User
|
|
25
|
-
attr_accessor :name, :email
|
|
26
|
-
|
|
27
|
-
def initialize(name:, email:)
|
|
28
|
-
@name = name
|
|
29
|
-
@email = email
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def valid?
|
|
33
|
-
return false if name.nil? || email.nil?
|
|
34
|
-
|
|
35
|
-
email.include?("@")
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
class PaymentProcessor
|
|
40
|
-
def charge(amount)
|
|
41
|
-
"real_charge_#{amount}"
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# 1. Core DSL, Context Isolation, Lazy Let Memoization, Matchers
|
|
46
|
-
Crspec.describe User, type: :model do
|
|
47
|
-
let(:valid_attributes) { { name: "Jane Doe", email: "jane@example.com" } }
|
|
48
|
-
subject(:user) { User.new(**valid_attributes) }
|
|
49
|
-
|
|
50
|
-
before do
|
|
51
|
-
# Per-example setup hook
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
it "validates primary attributes concurrently" do
|
|
55
|
-
expect(user.valid?).to be(true)
|
|
56
|
-
expect(user.name).to eq("Jane Doe")
|
|
57
|
-
expect(user.email).to include("example.com")
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
context "when email is invalid" do
|
|
61
|
-
let(:valid_attributes) { { name: "Jane Doe", email: nil } }
|
|
62
|
-
|
|
63
|
-
it "flags validation errors" do
|
|
64
|
-
expect(user.valid?).to eq(false)
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
context "when error is expected" do
|
|
69
|
-
it "catches raised exceptions" do
|
|
70
|
-
expect { raise ArgumentError, "Invalid user input" }.to raise_error(ArgumentError, "Invalid user input")
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# 2. Fiber-Aware Mocking Engine (crspec-mock)
|
|
76
|
-
Crspec.describe PaymentProcessor do
|
|
77
|
-
let(:processor) { PaymentProcessor.new }
|
|
78
|
-
|
|
79
|
-
it "supports fiber-isolated method stubs" do
|
|
80
|
-
allow(processor).to receive(:charge).with(5000).and_return("succeeded")
|
|
81
|
-
expect(processor.charge(5000)).to eq("succeeded")
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
it "supports test doubles and method expectations" do
|
|
85
|
-
gateway_double = double("StripeGateway", process: "ok")
|
|
86
|
-
expect(gateway_double.process).to eq("ok")
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# 3. Rails Parallel Worker Integration (crspec-rails)
|
|
91
|
-
Crspec::Rails::Parallel.parallelize(workers: 4) do
|
|
92
|
-
parallelize_setup do |_worker_num|
|
|
93
|
-
# Per-worker setup hook
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
parallelize_teardown do |_worker_num|
|
|
97
|
-
# Per-worker teardown hook
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# 4. Prism AST Transpiler Verification (crspec-transpiler)
|
|
102
|
-
rspec_sample_code = <<~RUBY
|
|
103
|
-
RSpec.describe "Legacy Suite" do
|
|
104
|
-
it "runs" do
|
|
105
|
-
expect(1).to eq(1)
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
RUBY
|
|
109
|
-
|
|
110
|
-
transpiled = Crspec::Transpiler::Rewriter.new(rspec_sample_code).transpile
|
|
111
|
-
puts "[Prism Transpiler Check] RSpec -> Crspec: #{transpiled.include?("Crspec.describe") ? "PASSED" : "FAILED"}"
|
|
112
|
-
|
|
113
|
-
# 5. Concurrent Multi-Threaded Execution Kernel
|
|
114
|
-
puts "\n[Executing Specs Concurrently Across 4 Worker Threads]..."
|
|
115
|
-
runner = Crspec::Runner.new(concurrency: 4)
|
|
116
|
-
runner.run(Crspec.world.example_groups)
|
|
117
|
-
|
|
118
|
-
puts "\n---------------- Execution Summary ----------------"
|
|
119
|
-
puts "Total Duration : #{runner.total_duration.round(4)}s"
|
|
120
|
-
puts "Passed Examples: #{runner.passed_examples.size}"
|
|
121
|
-
puts "Failed Examples: #{runner.failed_examples.size}"
|
|
122
|
-
puts "---------------------------------------------------"
|
|
123
|
-
|
|
124
|
-
if runner.success?
|
|
125
|
-
puts "\nSUCCESS: All specs executed concurrently across 4 worker threads and passed cleanly!"
|
|
126
|
-
else
|
|
127
|
-
puts "\nFAILURE: Some specs failed."
|
|
128
|
-
exit 1
|
|
129
|
-
end
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files.
|
|
2
|
-
|
|
3
|
-
# Ignore git directory.
|
|
4
|
-
/.git/
|
|
5
|
-
/.gitignore
|
|
6
|
-
|
|
7
|
-
# Ignore bundler config.
|
|
8
|
-
/.bundle
|
|
9
|
-
|
|
10
|
-
# Ignore all environment files.
|
|
11
|
-
/.env*
|
|
12
|
-
|
|
13
|
-
# Ignore all default key files.
|
|
14
|
-
/config/master.key
|
|
15
|
-
/config/credentials/*.key
|
|
16
|
-
|
|
17
|
-
# Ignore all logfiles and tempfiles.
|
|
18
|
-
/log/*
|
|
19
|
-
/tmp/*
|
|
20
|
-
!/log/.keep
|
|
21
|
-
!/tmp/.keep
|
|
22
|
-
|
|
23
|
-
# Ignore pidfiles, but keep the directory.
|
|
24
|
-
/tmp/pids/*
|
|
25
|
-
!/tmp/pids/.keep
|
|
26
|
-
|
|
27
|
-
# Ignore storage (uploaded files in development and any SQLite databases).
|
|
28
|
-
/storage/*
|
|
29
|
-
!/storage/.keep
|
|
30
|
-
/tmp/storage/*
|
|
31
|
-
!/tmp/storage/.keep
|
|
32
|
-
|
|
33
|
-
# Ignore assets.
|
|
34
|
-
/node_modules/
|
|
35
|
-
/app/assets/builds/*
|
|
36
|
-
!/app/assets/builds/.keep
|
|
37
|
-
/public/assets
|
|
38
|
-
|
|
39
|
-
# Ignore CI service files.
|
|
40
|
-
/.github
|
|
41
|
-
|
|
42
|
-
# Ignore Kamal files.
|
|
43
|
-
/config/deploy*.yml
|
|
44
|
-
/.kamal
|
|
45
|
-
|
|
46
|
-
# Ignore development files
|
|
47
|
-
/.devcontainer
|
|
48
|
-
|
|
49
|
-
# Ignore Docker-related files
|
|
50
|
-
/.dockerignore
|
|
51
|
-
/Dockerfile*
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# See https://git-scm.com/docs/gitattributes for more about git attribute files.
|
|
2
|
-
|
|
3
|
-
# Mark the database schema as having been generated.
|
|
4
|
-
db/schema.rb linguist-generated
|
|
5
|
-
|
|
6
|
-
# Mark any vendored files as having been vendored.
|
|
7
|
-
vendor/* linguist-vendored
|
|
8
|
-
config/credentials/*.yml.enc diff=rails_credentials
|
|
9
|
-
config/credentials.yml.enc diff=rails_credentials
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
pull_request:
|
|
5
|
-
push:
|
|
6
|
-
branches: [ main ]
|
|
7
|
-
|
|
8
|
-
jobs:
|
|
9
|
-
scan_ruby:
|
|
10
|
-
runs-on: ubuntu-latest
|
|
11
|
-
|
|
12
|
-
steps:
|
|
13
|
-
- name: Checkout code
|
|
14
|
-
uses: actions/checkout@v6
|
|
15
|
-
|
|
16
|
-
- name: Set up Ruby
|
|
17
|
-
uses: ruby/setup-ruby@v1
|
|
18
|
-
with:
|
|
19
|
-
bundler-cache: true
|
|
20
|
-
|
|
21
|
-
- name: Scan for common Rails security vulnerabilities using static analysis
|
|
22
|
-
run: bin/brakeman --no-pager
|
|
23
|
-
|
|
24
|
-
- name: Scan for known security vulnerabilities in gems used
|
|
25
|
-
run: bin/bundler-audit
|
|
26
|
-
|
|
27
|
-
scan_js:
|
|
28
|
-
runs-on: ubuntu-latest
|
|
29
|
-
|
|
30
|
-
steps:
|
|
31
|
-
- name: Checkout code
|
|
32
|
-
uses: actions/checkout@v6
|
|
33
|
-
|
|
34
|
-
- name: Set up Ruby
|
|
35
|
-
uses: ruby/setup-ruby@v1
|
|
36
|
-
with:
|
|
37
|
-
bundler-cache: true
|
|
38
|
-
|
|
39
|
-
- name: Scan for security vulnerabilities in JavaScript dependencies
|
|
40
|
-
run: bin/importmap audit
|
|
41
|
-
|
|
42
|
-
lint:
|
|
43
|
-
runs-on: ubuntu-latest
|
|
44
|
-
env:
|
|
45
|
-
RUBOCOP_CACHE_ROOT: tmp/rubocop
|
|
46
|
-
steps:
|
|
47
|
-
- name: Checkout code
|
|
48
|
-
uses: actions/checkout@v6
|
|
49
|
-
|
|
50
|
-
- name: Set up Ruby
|
|
51
|
-
uses: ruby/setup-ruby@v1
|
|
52
|
-
with:
|
|
53
|
-
bundler-cache: true
|
|
54
|
-
|
|
55
|
-
- name: Prepare RuboCop cache
|
|
56
|
-
uses: actions/cache@v4
|
|
57
|
-
env:
|
|
58
|
-
DEPENDENCIES_HASH: ${{ hashFiles('.ruby-version', '**/.rubocop.yml', '**/.rubocop_todo.yml', 'Gemfile.lock') }}
|
|
59
|
-
with:
|
|
60
|
-
path: ${{ env.RUBOCOP_CACHE_ROOT }}
|
|
61
|
-
key: rubocop-${{ runner.os }}-${{ env.DEPENDENCIES_HASH }}-${{ github.ref_name == github.event.repository.default_branch && github.run_id || 'default' }}
|
|
62
|
-
restore-keys: |
|
|
63
|
-
rubocop-${{ runner.os }}-${{ env.DEPENDENCIES_HASH }}-
|
|
64
|
-
|
|
65
|
-
- name: Lint code for consistent style
|
|
66
|
-
run: bin/rubocop -f github
|
|
67
|
-
|
|
68
|
-
test:
|
|
69
|
-
runs-on: ubuntu-latest
|
|
70
|
-
|
|
71
|
-
# services:
|
|
72
|
-
# redis:
|
|
73
|
-
# image: valkey/valkey:8
|
|
74
|
-
# ports:
|
|
75
|
-
# - 6379:6379
|
|
76
|
-
# options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
|
|
77
|
-
steps:
|
|
78
|
-
- name: Install packages
|
|
79
|
-
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libvips
|
|
80
|
-
|
|
81
|
-
- name: Checkout code
|
|
82
|
-
uses: actions/checkout@v6
|
|
83
|
-
|
|
84
|
-
- name: Set up Ruby
|
|
85
|
-
uses: ruby/setup-ruby@v1
|
|
86
|
-
with:
|
|
87
|
-
bundler-cache: true
|
|
88
|
-
|
|
89
|
-
- name: Run tests
|
|
90
|
-
env:
|
|
91
|
-
RAILS_ENV: test
|
|
92
|
-
# RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
|
|
93
|
-
# REDIS_URL: redis://localhost:6379/0
|
|
94
|
-
run: bin/rails db:test:prepare test
|
|
95
|
-
|
|
96
|
-
system-test:
|
|
97
|
-
runs-on: ubuntu-latest
|
|
98
|
-
|
|
99
|
-
# services:
|
|
100
|
-
# redis:
|
|
101
|
-
# image: valkey/valkey:8
|
|
102
|
-
# ports:
|
|
103
|
-
# - 6379:6379
|
|
104
|
-
# options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
|
|
105
|
-
steps:
|
|
106
|
-
- name: Install packages
|
|
107
|
-
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libvips
|
|
108
|
-
|
|
109
|
-
- name: Checkout code
|
|
110
|
-
uses: actions/checkout@v6
|
|
111
|
-
|
|
112
|
-
- name: Set up Ruby
|
|
113
|
-
uses: ruby/setup-ruby@v1
|
|
114
|
-
with:
|
|
115
|
-
bundler-cache: true
|
|
116
|
-
|
|
117
|
-
- name: Run System Tests
|
|
118
|
-
env:
|
|
119
|
-
RAILS_ENV: test
|
|
120
|
-
# RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
|
|
121
|
-
# REDIS_URL: redis://localhost:6379/0
|
|
122
|
-
run: bin/rails db:test:prepare test:system
|
|
123
|
-
|
|
124
|
-
- name: Keep screenshots from failed system tests
|
|
125
|
-
uses: actions/upload-artifact@v4
|
|
126
|
-
if: failure()
|
|
127
|
-
with:
|
|
128
|
-
name: screenshots
|
|
129
|
-
path: ${{ github.workspace }}/tmp/screenshots
|
|
130
|
-
if-no-files-found: ignore
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
|
|
2
|
-
#
|
|
3
|
-
# Temporary files generated by your text editor or operating system
|
|
4
|
-
# belong in git's global ignore instead:
|
|
5
|
-
# `$XDG_CONFIG_HOME/git/ignore` or `~/.config/git/ignore`
|
|
6
|
-
|
|
7
|
-
# Ignore bundler config.
|
|
8
|
-
/.bundle
|
|
9
|
-
|
|
10
|
-
# Ignore all environment files.
|
|
11
|
-
/.env*
|
|
12
|
-
|
|
13
|
-
# Ignore all logfiles and tempfiles.
|
|
14
|
-
/log/*
|
|
15
|
-
/tmp/*
|
|
16
|
-
!/log/.keep
|
|
17
|
-
!/tmp/.keep
|
|
18
|
-
|
|
19
|
-
# Ignore pidfiles, but keep the directory.
|
|
20
|
-
/tmp/pids/*
|
|
21
|
-
!/tmp/pids/
|
|
22
|
-
!/tmp/pids/.keep
|
|
23
|
-
|
|
24
|
-
# Ignore storage (uploaded files in development and any SQLite databases).
|
|
25
|
-
/storage/*
|
|
26
|
-
!/storage/.keep
|
|
27
|
-
/tmp/storage/*
|
|
28
|
-
!/tmp/storage/
|
|
29
|
-
!/tmp/storage/.keep
|
|
30
|
-
|
|
31
|
-
/public/assets
|
|
32
|
-
|
|
33
|
-
# Ignore key files for decrypting credentials and more.
|
|
34
|
-
/config/*.key
|
|
35
|
-
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env sh
|
|
2
|
-
|
|
3
|
-
# A sample post-deploy hook
|
|
4
|
-
#
|
|
5
|
-
# These environment variables are available:
|
|
6
|
-
# KAMAL_RECORDED_AT
|
|
7
|
-
# KAMAL_PERFORMER
|
|
8
|
-
# KAMAL_VERSION
|
|
9
|
-
# KAMAL_HOSTS
|
|
10
|
-
# KAMAL_ROLES (if set)
|
|
11
|
-
# KAMAL_DESTINATION (if set)
|
|
12
|
-
# KAMAL_RUNTIME
|
|
13
|
-
|
|
14
|
-
echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds"
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env sh
|
|
2
|
-
|
|
3
|
-
# A sample pre-build hook
|
|
4
|
-
#
|
|
5
|
-
# Checks:
|
|
6
|
-
# 1. We have a clean checkout
|
|
7
|
-
# 2. A remote is configured
|
|
8
|
-
# 3. The branch has been pushed to the remote
|
|
9
|
-
# 4. The version we are deploying matches the remote
|
|
10
|
-
#
|
|
11
|
-
# These environment variables are available:
|
|
12
|
-
# KAMAL_RECORDED_AT
|
|
13
|
-
# KAMAL_PERFORMER
|
|
14
|
-
# KAMAL_VERSION
|
|
15
|
-
# KAMAL_HOSTS
|
|
16
|
-
# KAMAL_ROLES (if set)
|
|
17
|
-
# KAMAL_DESTINATION (if set)
|
|
18
|
-
|
|
19
|
-
if [ -n "$(git status --porcelain)" ]; then
|
|
20
|
-
echo "Git checkout is not clean, aborting..." >&2
|
|
21
|
-
git status --porcelain >&2
|
|
22
|
-
exit 1
|
|
23
|
-
fi
|
|
24
|
-
|
|
25
|
-
first_remote=$(git remote)
|
|
26
|
-
|
|
27
|
-
if [ -z "$first_remote" ]; then
|
|
28
|
-
echo "No git remote set, aborting..." >&2
|
|
29
|
-
exit 1
|
|
30
|
-
fi
|
|
31
|
-
|
|
32
|
-
current_branch=$(git branch --show-current)
|
|
33
|
-
|
|
34
|
-
if [ -z "$current_branch" ]; then
|
|
35
|
-
echo "Not on a git branch, aborting..." >&2
|
|
36
|
-
exit 1
|
|
37
|
-
fi
|
|
38
|
-
|
|
39
|
-
remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1)
|
|
40
|
-
|
|
41
|
-
if [ -z "$remote_head" ]; then
|
|
42
|
-
echo "Branch not pushed to remote, aborting..." >&2
|
|
43
|
-
exit 1
|
|
44
|
-
fi
|
|
45
|
-
|
|
46
|
-
if [ "$KAMAL_VERSION" != "$remote_head" ]; then
|
|
47
|
-
echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2
|
|
48
|
-
exit 1
|
|
49
|
-
fi
|
|
50
|
-
|
|
51
|
-
exit 0
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
|
|
3
|
-
# A sample pre-connect check
|
|
4
|
-
#
|
|
5
|
-
# Warms DNS before connecting to hosts in parallel
|
|
6
|
-
#
|
|
7
|
-
# These environment variables are available:
|
|
8
|
-
# KAMAL_RECORDED_AT
|
|
9
|
-
# KAMAL_PERFORMER
|
|
10
|
-
# KAMAL_VERSION
|
|
11
|
-
# KAMAL_HOSTS
|
|
12
|
-
# KAMAL_ROLES (if set)
|
|
13
|
-
# KAMAL_DESTINATION (if set)
|
|
14
|
-
# KAMAL_RUNTIME
|
|
15
|
-
|
|
16
|
-
hosts = ENV["KAMAL_HOSTS"].split(",")
|
|
17
|
-
results = nil
|
|
18
|
-
max = 3
|
|
19
|
-
|
|
20
|
-
elapsed = Benchmark.realtime do
|
|
21
|
-
results = hosts.map do |host|
|
|
22
|
-
Thread.new do
|
|
23
|
-
tries = 1
|
|
24
|
-
|
|
25
|
-
begin
|
|
26
|
-
Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME)
|
|
27
|
-
rescue SocketError
|
|
28
|
-
if tries < max
|
|
29
|
-
puts "Retrying DNS warmup: #{host}"
|
|
30
|
-
tries += 1
|
|
31
|
-
sleep rand
|
|
32
|
-
retry
|
|
33
|
-
else
|
|
34
|
-
puts "DNS warmup failed: #{host}"
|
|
35
|
-
host
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
tries
|
|
40
|
-
end
|
|
41
|
-
end.map(&:value)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
retries = results.sum - hosts.size
|
|
45
|
-
nopes = results.count { |r| r == max }
|
|
46
|
-
|
|
47
|
-
puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ]
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
|
|
3
|
-
# A sample pre-deploy hook
|
|
4
|
-
#
|
|
5
|
-
# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds.
|
|
6
|
-
#
|
|
7
|
-
# Fails unless the combined status is "success"
|
|
8
|
-
#
|
|
9
|
-
# These environment variables are available:
|
|
10
|
-
# KAMAL_RECORDED_AT
|
|
11
|
-
# KAMAL_PERFORMER
|
|
12
|
-
# KAMAL_VERSION
|
|
13
|
-
# KAMAL_HOSTS
|
|
14
|
-
# KAMAL_COMMAND
|
|
15
|
-
# KAMAL_SUBCOMMAND
|
|
16
|
-
# KAMAL_ROLES (if set)
|
|
17
|
-
# KAMAL_DESTINATION (if set)
|
|
18
|
-
|
|
19
|
-
# Only check the build status for production deployments
|
|
20
|
-
if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production"
|
|
21
|
-
exit 0
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
require "bundler/inline"
|
|
25
|
-
|
|
26
|
-
# true = install gems so this is fast on repeat invocations
|
|
27
|
-
gemfile(true, quiet: true) do
|
|
28
|
-
source "https://rubygems.org"
|
|
29
|
-
|
|
30
|
-
gem "octokit"
|
|
31
|
-
gem "faraday-retry"
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
MAX_ATTEMPTS = 72
|
|
35
|
-
ATTEMPTS_GAP = 10
|
|
36
|
-
|
|
37
|
-
def exit_with_error(message)
|
|
38
|
-
$stderr.puts message
|
|
39
|
-
exit 1
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
class GithubStatusChecks
|
|
43
|
-
attr_reader :remote_url, :git_sha, :github_client, :combined_status
|
|
44
|
-
|
|
45
|
-
def initialize
|
|
46
|
-
@remote_url = github_repo_from_remote_url
|
|
47
|
-
@git_sha = `git rev-parse HEAD`.strip
|
|
48
|
-
@github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"])
|
|
49
|
-
refresh!
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def refresh!
|
|
53
|
-
@combined_status = github_client.combined_status(remote_url, git_sha)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def state
|
|
57
|
-
combined_status[:state]
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def first_status_url
|
|
61
|
-
first_status = combined_status[:statuses].find { |status| status[:state] == state }
|
|
62
|
-
first_status && first_status[:target_url]
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def complete_count
|
|
66
|
-
combined_status[:statuses].count { |status| status[:state] != "pending"}
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def total_count
|
|
70
|
-
combined_status[:statuses].count
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def current_status
|
|
74
|
-
if total_count > 0
|
|
75
|
-
"Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..."
|
|
76
|
-
else
|
|
77
|
-
"Build not started..."
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
private
|
|
82
|
-
def github_repo_from_remote_url
|
|
83
|
-
url = `git config --get remote.origin.url`.strip.delete_suffix(".git")
|
|
84
|
-
if url.start_with?("https://github.com/")
|
|
85
|
-
url.delete_prefix("https://github.com/")
|
|
86
|
-
elsif url.start_with?("git@github.com:")
|
|
87
|
-
url.delete_prefix("git@github.com:")
|
|
88
|
-
else
|
|
89
|
-
url
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
$stdout.sync = true
|
|
96
|
-
|
|
97
|
-
begin
|
|
98
|
-
puts "Checking build status..."
|
|
99
|
-
|
|
100
|
-
attempts = 0
|
|
101
|
-
checks = GithubStatusChecks.new
|
|
102
|
-
|
|
103
|
-
loop do
|
|
104
|
-
case checks.state
|
|
105
|
-
when "success"
|
|
106
|
-
puts "Checks passed, see #{checks.first_status_url}"
|
|
107
|
-
exit 0
|
|
108
|
-
when "failure"
|
|
109
|
-
exit_with_error "Checks failed, see #{checks.first_status_url}"
|
|
110
|
-
when "pending"
|
|
111
|
-
attempts += 1
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS
|
|
115
|
-
|
|
116
|
-
puts checks.current_status
|
|
117
|
-
sleep(ATTEMPTS_GAP)
|
|
118
|
-
checks.refresh!
|
|
119
|
-
end
|
|
120
|
-
rescue Octokit::NotFound
|
|
121
|
-
exit_with_error "Build status could not be found"
|
|
122
|
-
end
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets,
|
|
2
|
-
# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either
|
|
3
|
-
# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git.
|
|
4
|
-
|
|
5
|
-
# Example of extracting secrets from 1password (or another compatible pw manager)
|
|
6
|
-
# SECRETS=$(kamal secrets fetch --adapter 1password --account your-account --from Vault/Item KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY)
|
|
7
|
-
# KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${SECRETS})
|
|
8
|
-
# RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY ${SECRETS})
|
|
9
|
-
|
|
10
|
-
# Example of extracting secrets from Rails credentials
|
|
11
|
-
# KAMAL_REGISTRY_PASSWORD=$(rails credentials:fetch kamal.registry_password)
|
|
12
|
-
|
|
13
|
-
# Use a GITHUB_TOKEN if private repositories are needed for the image
|
|
14
|
-
# GITHUB_TOKEN=$(gh config get -h github.com oauth_token)
|
|
15
|
-
|
|
16
|
-
# Grab the registry password from ENV
|
|
17
|
-
# KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
|
|
18
|
-
|
|
19
|
-
# Improve security by using a password manager. Never check config/master.key into git!
|
|
20
|
-
RAILS_MASTER_KEY=$(cat config/master.key)
|