pg_multitenant_schemas 0.1.3 โ 0.2.2
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/.actrc +17 -0
- data/.env.local.example +21 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +86 -0
- data/LOCAL_TESTING_SUMMARY.md +141 -0
- data/README.md +269 -16
- data/TESTING_LOCALLY.md +208 -0
- data/docs/README.md +81 -0
- data/docs/configuration.md +340 -0
- data/docs/context.md +292 -0
- data/docs/errors.md +498 -0
- data/docs/github_actions_permissions_fix.md +136 -0
- data/docs/github_actions_setup.md +181 -0
- data/docs/integration_testing.md +454 -0
- data/docs/local_workflow_testing.md +314 -0
- data/docs/migrator.md +291 -0
- data/docs/rails_integration.md +468 -0
- data/docs/schema_switcher.md +182 -0
- data/docs/tenant_resolver.md +394 -0
- data/docs/testing.md +358 -0
- data/examples/context_management.rb +198 -0
- data/examples/migration_workflow.rb +50 -0
- data/examples/rails_integration/controller_examples.rb +368 -0
- data/examples/schema_operations.rb +124 -0
- data/lib/pg_multitenant_schemas/configuration.rb +4 -4
- data/lib/pg_multitenant_schemas/migration_display_reporter.rb +30 -0
- data/lib/pg_multitenant_schemas/migration_executor.rb +81 -0
- data/lib/pg_multitenant_schemas/migration_schema_operations.rb +54 -0
- data/lib/pg_multitenant_schemas/migration_status_reporter.rb +65 -0
- data/lib/pg_multitenant_schemas/migrator.rb +89 -0
- data/lib/pg_multitenant_schemas/schema_switcher.rb +40 -66
- data/lib/pg_multitenant_schemas/tasks/advanced_tasks.rake +21 -0
- data/lib/pg_multitenant_schemas/tasks/basic_tasks.rake +20 -0
- data/lib/pg_multitenant_schemas/tasks/pg_multitenant_schemas.rake +53 -143
- data/lib/pg_multitenant_schemas/tasks/tenant_tasks.rake +65 -0
- data/lib/pg_multitenant_schemas/tenant_task_helpers.rb +102 -0
- data/lib/pg_multitenant_schemas/version.rb +1 -1
- data/lib/pg_multitenant_schemas.rb +10 -5
- data/pg_multitenant_schemas.gemspec +10 -9
- data/pre-push-check.sh +95 -0
- data/rails_integration/app/controllers/application_controller.rb +6 -0
- data/rails_integration/app/models/tenant.rb +6 -0
- data/test-github-setup.sh +85 -0
- data/validate-github-commands.sh +47 -0
- metadata +49 -17
data/docs/errors.md
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
# Errors - Exception Handling and Custom Errors
|
|
2
|
+
|
|
3
|
+
**File**: `lib/pg_multitenant_schemas/errors.rb`
|
|
4
|
+
|
|
5
|
+
## ๐ Overview
|
|
6
|
+
|
|
7
|
+
The `Errors` module defines custom exception classes for the PG Multitenant Schemas gem, providing specific error types for different failure scenarios and better error handling throughout the application.
|
|
8
|
+
|
|
9
|
+
## ๐ฏ Purpose
|
|
10
|
+
|
|
11
|
+
- **Specific Exceptions**: Different error types for different failure scenarios
|
|
12
|
+
- **Better Debugging**: Clear error messages with context information
|
|
13
|
+
- **Error Handling**: Structured error handling throughout the gem
|
|
14
|
+
- **User Experience**: Meaningful error messages for developers
|
|
15
|
+
|
|
16
|
+
## ๐ง Exception Classes
|
|
17
|
+
|
|
18
|
+
### Core Exceptions
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
module PgMultitenantSchemas
|
|
22
|
+
# Base exception class for all gem-related errors
|
|
23
|
+
class Error < StandardError; end
|
|
24
|
+
|
|
25
|
+
# Configuration-related errors
|
|
26
|
+
class ConfigurationError < Error; end
|
|
27
|
+
|
|
28
|
+
# Schema operation failures
|
|
29
|
+
class SchemaError < Error; end
|
|
30
|
+
|
|
31
|
+
# Tenant resolution failures
|
|
32
|
+
class TenantNotFoundError < Error; end
|
|
33
|
+
|
|
34
|
+
# Migration-related errors
|
|
35
|
+
class MigrationError < Error; end
|
|
36
|
+
|
|
37
|
+
# Connection-related errors
|
|
38
|
+
class ConnectionError < Error; end
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Detailed Exception Definitions
|
|
43
|
+
|
|
44
|
+
#### Base Error Class
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
class Error < StandardError
|
|
48
|
+
attr_reader :context, :tenant, :schema
|
|
49
|
+
|
|
50
|
+
def initialize(message, context: nil, tenant: nil, schema: nil)
|
|
51
|
+
@context = context
|
|
52
|
+
@tenant = tenant
|
|
53
|
+
@schema = schema
|
|
54
|
+
super(message)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def to_h
|
|
58
|
+
{
|
|
59
|
+
error: self.class.name,
|
|
60
|
+
message: message,
|
|
61
|
+
context: context,
|
|
62
|
+
tenant: tenant&.to_s,
|
|
63
|
+
schema: schema
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
#### Configuration Errors
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
class ConfigurationError < Error
|
|
73
|
+
def self.missing_tenant_model
|
|
74
|
+
new("Tenant model not configured. Set config.tenant_model in initializer.")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.invalid_connection_class(class_name)
|
|
78
|
+
new("Invalid connection class: #{class_name}. Class not found or not an ActiveRecord class.")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.missing_default_schema
|
|
82
|
+
new("Default schema not configured. Set config.default_schema in initializer.")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
#### Schema Errors
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
class SchemaError < Error
|
|
91
|
+
def self.schema_not_found(schema_name)
|
|
92
|
+
new("Schema '#{schema_name}' does not exist", schema: schema_name)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.schema_already_exists(schema_name)
|
|
96
|
+
new("Schema '#{schema_name}' already exists", schema: schema_name)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def self.invalid_schema_name(schema_name)
|
|
100
|
+
new("Invalid schema name: '#{schema_name}'. Schema names must be valid PostgreSQL identifiers.", schema: schema_name)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def self.cannot_drop_public_schema
|
|
104
|
+
new("Cannot drop the public schema", schema: 'public')
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def self.schema_switch_failed(schema_name, original_error)
|
|
108
|
+
new("Failed to switch to schema '#{schema_name}': #{original_error.message}", schema: schema_name)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### Tenant Errors
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
class TenantNotFoundError < Error
|
|
117
|
+
def self.by_subdomain(subdomain)
|
|
118
|
+
new("Tenant not found with subdomain: #{subdomain}", context: { subdomain: subdomain })
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def self.by_domain(domain)
|
|
122
|
+
new("Tenant not found with domain: #{domain}", context: { domain: domain })
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def self.by_id(tenant_id)
|
|
126
|
+
new("Tenant not found with ID: #{tenant_id}", context: { tenant_id: tenant_id })
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def self.from_request(request_info)
|
|
130
|
+
new("Could not resolve tenant from request", context: request_info)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
#### Migration Errors
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
class MigrationError < Error
|
|
139
|
+
def self.migration_failed(schema_name, migration_version, original_error)
|
|
140
|
+
new(
|
|
141
|
+
"Migration #{migration_version} failed for schema #{schema_name}: #{original_error.message}",
|
|
142
|
+
schema: schema_name,
|
|
143
|
+
context: { migration_version: migration_version, original_error: original_error.class.name }
|
|
144
|
+
)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def self.rollback_failed(schema_name, steps, original_error)
|
|
148
|
+
new(
|
|
149
|
+
"Failed to rollback #{steps} steps for schema #{schema_name}: #{original_error.message}",
|
|
150
|
+
schema: schema_name,
|
|
151
|
+
context: { rollback_steps: steps, original_error: original_error.class.name }
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def self.no_pending_migrations(schema_name)
|
|
156
|
+
new("No pending migrations for schema: #{schema_name}", schema: schema_name)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
#### Connection Errors
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
class ConnectionError < Error
|
|
165
|
+
def self.connection_not_available
|
|
166
|
+
new("Database connection not available")
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def self.invalid_connection_config(config)
|
|
170
|
+
new("Invalid connection configuration: #{config}")
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def self.connection_timeout
|
|
174
|
+
new("Database connection timeout")
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## ๐ Usage Patterns
|
|
180
|
+
|
|
181
|
+
### Error Handling in Components
|
|
182
|
+
|
|
183
|
+
#### Schema Switcher Error Handling
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
module PgMultitenantSchemas
|
|
187
|
+
class SchemaSwitcher
|
|
188
|
+
def self.switch_schema(schema_name)
|
|
189
|
+
validate_schema_name!(schema_name)
|
|
190
|
+
|
|
191
|
+
connection.execute("SET search_path TO #{schema_name}, public")
|
|
192
|
+
rescue PG::UndefinedObject => e
|
|
193
|
+
raise SchemaError.schema_not_found(schema_name)
|
|
194
|
+
rescue StandardError => e
|
|
195
|
+
raise SchemaError.schema_switch_failed(schema_name, e)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def self.create_schema(schema_name)
|
|
199
|
+
validate_schema_name!(schema_name)
|
|
200
|
+
|
|
201
|
+
connection.execute("CREATE SCHEMA IF NOT EXISTS #{schema_name}")
|
|
202
|
+
rescue PG::DuplicateSchema => e
|
|
203
|
+
raise SchemaError.schema_already_exists(schema_name)
|
|
204
|
+
rescue StandardError => e
|
|
205
|
+
raise SchemaError.new("Failed to create schema #{schema_name}: #{e.message}", schema: schema_name)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
private
|
|
209
|
+
|
|
210
|
+
def self.validate_schema_name!(schema_name)
|
|
211
|
+
if schema_name.blank? || !schema_name.match?(/\A[a-zA-Z_][a-zA-Z0-9_]*\z/)
|
|
212
|
+
raise SchemaError.invalid_schema_name(schema_name)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
#### Tenant Resolver Error Handling
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
module PgMultitenantSchemas
|
|
223
|
+
class TenantResolver
|
|
224
|
+
def self.resolve_from_request(request)
|
|
225
|
+
subdomain = extract_subdomain(request)
|
|
226
|
+
return nil if subdomain.blank?
|
|
227
|
+
|
|
228
|
+
tenant = tenant_model.find_by(subdomain: subdomain)
|
|
229
|
+
|
|
230
|
+
unless tenant
|
|
231
|
+
raise TenantNotFoundError.by_subdomain(subdomain)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
tenant
|
|
235
|
+
rescue ActiveRecord::ConnectionNotEstablished => e
|
|
236
|
+
raise ConnectionError.connection_not_available
|
|
237
|
+
rescue StandardError => e
|
|
238
|
+
request_info = {
|
|
239
|
+
host: request.host,
|
|
240
|
+
subdomain: subdomain,
|
|
241
|
+
user_agent: request.user_agent
|
|
242
|
+
}
|
|
243
|
+
raise TenantNotFoundError.from_request(request_info)
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
#### Migrator Error Handling
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
module PgMultitenantSchemas
|
|
253
|
+
class Migrator
|
|
254
|
+
def self.migrate_tenant(schema_name)
|
|
255
|
+
puts "๐ฆ Migrating schema: #{schema_name}"
|
|
256
|
+
|
|
257
|
+
Context.with_tenant(schema_name) do
|
|
258
|
+
ActiveRecord::Tasks::DatabaseTasks.migrate
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
puts " โ
Completed migration for #{schema_name}"
|
|
262
|
+
rescue StandardError => e
|
|
263
|
+
puts " โ Error migrating #{schema_name}: #{e.message}"
|
|
264
|
+
|
|
265
|
+
# Re-raise as MigrationError with context
|
|
266
|
+
raise MigrationError.migration_failed(schema_name, 'latest', e)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Application-Level Error Handling
|
|
273
|
+
|
|
274
|
+
#### Controller Error Handling
|
|
275
|
+
|
|
276
|
+
```ruby
|
|
277
|
+
class ApplicationController < ActionController::Base
|
|
278
|
+
rescue_from PgMultitenantSchemas::TenantNotFoundError, with: :handle_tenant_not_found
|
|
279
|
+
rescue_from PgMultitenantSchemas::SchemaError, with: :handle_schema_error
|
|
280
|
+
rescue_from PgMultitenantSchemas::Error, with: :handle_multitenant_error
|
|
281
|
+
|
|
282
|
+
private
|
|
283
|
+
|
|
284
|
+
def handle_tenant_not_found(error)
|
|
285
|
+
Rails.logger.warn "Tenant not found: #{error.message}"
|
|
286
|
+
|
|
287
|
+
respond_to do |format|
|
|
288
|
+
format.html { redirect_to tenant_selection_path, alert: "Please select a valid tenant" }
|
|
289
|
+
format.json { render json: error.to_h, status: :not_found }
|
|
290
|
+
format.xml { render xml: error.to_h, status: :not_found }
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def handle_schema_error(error)
|
|
295
|
+
Rails.logger.error "Schema error: #{error.message}"
|
|
296
|
+
|
|
297
|
+
respond_to do |format|
|
|
298
|
+
format.html { redirect_to root_path, alert: "A database error occurred" }
|
|
299
|
+
format.json { render json: { error: "Database error" }, status: :internal_server_error }
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def handle_multitenant_error(error)
|
|
304
|
+
Rails.logger.error "Multitenant error: #{error.message}"
|
|
305
|
+
Rails.logger.error error.backtrace.join("\n")
|
|
306
|
+
|
|
307
|
+
respond_to do |format|
|
|
308
|
+
format.html { redirect_to root_path, alert: "An error occurred" }
|
|
309
|
+
format.json { render json: { error: "Internal error" }, status: :internal_server_error }
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
#### API Error Handling
|
|
316
|
+
|
|
317
|
+
```ruby
|
|
318
|
+
class Api::BaseController < ActionController::API
|
|
319
|
+
rescue_from PgMultitenantSchemas::TenantNotFoundError do |error|
|
|
320
|
+
render json: {
|
|
321
|
+
error: 'tenant_not_found',
|
|
322
|
+
message: error.message,
|
|
323
|
+
context: error.context
|
|
324
|
+
}, status: :not_found
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
rescue_from PgMultitenantSchemas::SchemaError do |error|
|
|
328
|
+
render json: {
|
|
329
|
+
error: 'schema_error',
|
|
330
|
+
message: error.message,
|
|
331
|
+
schema: error.schema
|
|
332
|
+
}, status: :unprocessable_entity
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
rescue_from PgMultitenantSchemas::MigrationError do |error|
|
|
336
|
+
render json: {
|
|
337
|
+
error: 'migration_error',
|
|
338
|
+
message: 'Database migration required',
|
|
339
|
+
schema: error.schema
|
|
340
|
+
}, status: :service_unavailable
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
rescue_from PgMultitenantSchemas::Error do |error|
|
|
344
|
+
render json: {
|
|
345
|
+
error: 'multitenant_error',
|
|
346
|
+
message: error.message
|
|
347
|
+
}, status: :internal_server_error
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
#### Background Job Error Handling
|
|
353
|
+
|
|
354
|
+
```ruby
|
|
355
|
+
class TenantJob < ApplicationJob
|
|
356
|
+
retry_on PgMultitenantSchemas::ConnectionError, wait: 30.seconds, attempts: 3
|
|
357
|
+
discard_on PgMultitenantSchemas::TenantNotFoundError
|
|
358
|
+
|
|
359
|
+
rescue_from PgMultitenantSchemas::SchemaError do |error|
|
|
360
|
+
Rails.logger.error "Schema error in job: #{error.message}"
|
|
361
|
+
# Notify administrators
|
|
362
|
+
AdminNotifier.schema_error(error).deliver_now
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def perform(tenant_id, *args)
|
|
366
|
+
tenant = Tenant.find(tenant_id)
|
|
367
|
+
|
|
368
|
+
PgMultitenantSchemas::Context.with_tenant(tenant) do
|
|
369
|
+
process_tenant_work(*args)
|
|
370
|
+
end
|
|
371
|
+
rescue PgMultitenantSchemas::TenantNotFoundError => e
|
|
372
|
+
Rails.logger.error "Tenant #{tenant_id} not found for job"
|
|
373
|
+
# Don't retry for missing tenants
|
|
374
|
+
raise e
|
|
375
|
+
rescue PgMultitenantSchemas::MigrationError => e
|
|
376
|
+
Rails.logger.error "Migration required for tenant #{tenant_id}"
|
|
377
|
+
# Schedule migration and retry
|
|
378
|
+
TenantMigrationJob.perform_later(tenant_id)
|
|
379
|
+
raise e
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## ๐ Error Monitoring and Debugging
|
|
385
|
+
|
|
386
|
+
### Error Logging Enhancement
|
|
387
|
+
|
|
388
|
+
```ruby
|
|
389
|
+
module ErrorLogging
|
|
390
|
+
def self.included(base)
|
|
391
|
+
base.extend(ClassMethods)
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
module ClassMethods
|
|
395
|
+
def with_error_context(context = {})
|
|
396
|
+
yield
|
|
397
|
+
rescue PgMultitenantSchemas::Error => e
|
|
398
|
+
enhanced_error = e.class.new(
|
|
399
|
+
e.message,
|
|
400
|
+
context: e.context&.merge(context) || context,
|
|
401
|
+
tenant: e.tenant,
|
|
402
|
+
schema: e.schema
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
Rails.logger.error "#{e.class.name}: #{e.message}"
|
|
406
|
+
Rails.logger.error "Context: #{enhanced_error.context}"
|
|
407
|
+
Rails.logger.error "Tenant: #{enhanced_error.tenant}" if enhanced_error.tenant
|
|
408
|
+
Rails.logger.error "Schema: #{enhanced_error.schema}" if enhanced_error.schema
|
|
409
|
+
|
|
410
|
+
raise enhanced_error
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### Error Reporting Integration
|
|
417
|
+
|
|
418
|
+
```ruby
|
|
419
|
+
# For services like Sentry, Rollbar, etc.
|
|
420
|
+
module ErrorReporting
|
|
421
|
+
def self.report_multitenant_error(error, extra_context = {})
|
|
422
|
+
return unless error.is_a?(PgMultitenantSchemas::Error)
|
|
423
|
+
|
|
424
|
+
Sentry.capture_exception(error) do |scope|
|
|
425
|
+
scope.set_tag('component', 'pg_multitenant_schemas')
|
|
426
|
+
scope.set_tag('tenant', error.tenant) if error.tenant
|
|
427
|
+
scope.set_tag('schema', error.schema) if error.schema
|
|
428
|
+
|
|
429
|
+
scope.set_context('multitenant', {
|
|
430
|
+
context: error.context,
|
|
431
|
+
tenant: error.tenant,
|
|
432
|
+
schema: error.schema
|
|
433
|
+
}.merge(extra_context))
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# Usage in error handlers
|
|
439
|
+
rescue PgMultitenantSchemas::Error => e
|
|
440
|
+
ErrorReporting.report_multitenant_error(e, { controller: self.class.name, action: action_name })
|
|
441
|
+
raise e
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
## ๐จ Error Prevention Best Practices
|
|
445
|
+
|
|
446
|
+
### Validation and Guards
|
|
447
|
+
|
|
448
|
+
```ruby
|
|
449
|
+
# Validate configuration on startup
|
|
450
|
+
PgMultitenantSchemas.configure do |config|
|
|
451
|
+
config.validate_on_startup = true
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
# Guard clauses in critical methods
|
|
455
|
+
def switch_to_tenant(tenant)
|
|
456
|
+
raise ArgumentError, "Tenant cannot be nil" if tenant.nil?
|
|
457
|
+
raise PgMultitenantSchemas::TenantNotFoundError.by_id(tenant.id) unless tenant.persisted?
|
|
458
|
+
|
|
459
|
+
# Proceed with tenant switching
|
|
460
|
+
end
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### Error Recovery
|
|
464
|
+
|
|
465
|
+
```ruby
|
|
466
|
+
# Automatic recovery for transient errors
|
|
467
|
+
module AutoRecovery
|
|
468
|
+
def with_retry(max_attempts: 3, backoff: 1.second)
|
|
469
|
+
attempts = 0
|
|
470
|
+
|
|
471
|
+
begin
|
|
472
|
+
yield
|
|
473
|
+
rescue PgMultitenantSchemas::ConnectionError => e
|
|
474
|
+
attempts += 1
|
|
475
|
+
|
|
476
|
+
if attempts < max_attempts
|
|
477
|
+
Rails.logger.warn "Connection error (attempt #{attempts}/#{max_attempts}): #{e.message}"
|
|
478
|
+
sleep(backoff * attempts)
|
|
479
|
+
retry
|
|
480
|
+
else
|
|
481
|
+
raise e
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
## ๐ Related Components
|
|
489
|
+
|
|
490
|
+
- **[Configuration](configuration.md)**: Configuration validation and errors
|
|
491
|
+
- **[SchemaSwitcher](schema_switcher.md)**: Schema operation error handling
|
|
492
|
+
- **[Context](context.md)**: Context management error scenarios
|
|
493
|
+
- **[TenantResolver](tenant_resolver.md)**: Tenant resolution error handling
|
|
494
|
+
- **[Migrator](migrator.md)**: Migration error management
|
|
495
|
+
|
|
496
|
+
## ๐ Examples
|
|
497
|
+
|
|
498
|
+
See [examples/error_handling.rb](../examples/) for comprehensive error handling patterns and recovery strategies.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# ๐ง Fixing GitHub Actions Permissions Issues
|
|
2
|
+
|
|
3
|
+
## ๐จ Problem
|
|
4
|
+
```
|
|
5
|
+
remote: Permission to rubenpazch/pg_multitenant_schemas.git denied to github-actions[bot].
|
|
6
|
+
fatal: unable to access 'https://github.com/rubenpazch/pg_multitenant_schemas/': The requested URL returned error: 403
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## โ
Solutions Applied
|
|
10
|
+
|
|
11
|
+
### 1. Updated Release Workflow Permissions
|
|
12
|
+
Added explicit permissions to `.github/workflows/release.yml`:
|
|
13
|
+
```yaml
|
|
14
|
+
permissions:
|
|
15
|
+
contents: write
|
|
16
|
+
issues: write
|
|
17
|
+
pull-requests: write
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### 2. Fixed GitHub Actions Bot Identity
|
|
21
|
+
Changed Git configuration to use proper GitHub Actions bot identity:
|
|
22
|
+
```yaml
|
|
23
|
+
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
24
|
+
git config --local user.name "github-actions[bot]"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## ๐ง Repository Settings to Check
|
|
28
|
+
|
|
29
|
+
### On GitHub.com:
|
|
30
|
+
|
|
31
|
+
1. **Go to your repository settings**
|
|
32
|
+
- Navigate to: `https://github.com/rubenpazch/pg_multitenant_schemas/settings`
|
|
33
|
+
|
|
34
|
+
2. **Actions > General**
|
|
35
|
+
- Scroll down to "Workflow permissions"
|
|
36
|
+
- Select: **"Read and write permissions"**
|
|
37
|
+
- Check: **"Allow GitHub Actions to create and approve pull requests"**
|
|
38
|
+
- Click **Save**
|
|
39
|
+
|
|
40
|
+
3. **Actions > General > Actions permissions**
|
|
41
|
+
- Ensure: **"Allow all actions and reusable workflows"** is selected
|
|
42
|
+
- Or at least: **"Allow actions created by GitHub"**
|
|
43
|
+
|
|
44
|
+
## ๐งช Alternative Testing Approach
|
|
45
|
+
|
|
46
|
+
If you want to test the release workflow without actual releases:
|
|
47
|
+
|
|
48
|
+
### Create a Test Branch Release Workflow
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
# .github/workflows/test-release.yml
|
|
52
|
+
name: Test Release Workflow
|
|
53
|
+
|
|
54
|
+
on:
|
|
55
|
+
workflow_dispatch: # Manual trigger only
|
|
56
|
+
|
|
57
|
+
permissions:
|
|
58
|
+
contents: write
|
|
59
|
+
issues: write
|
|
60
|
+
pull-requests: write
|
|
61
|
+
|
|
62
|
+
jobs:
|
|
63
|
+
test-release:
|
|
64
|
+
runs-on: ubuntu-latest
|
|
65
|
+
|
|
66
|
+
steps:
|
|
67
|
+
- name: Checkout code
|
|
68
|
+
uses: actions/checkout@v4
|
|
69
|
+
with:
|
|
70
|
+
fetch-depth: 0
|
|
71
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
72
|
+
|
|
73
|
+
- name: Test Git config
|
|
74
|
+
run: |
|
|
75
|
+
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
76
|
+
git config --local user.name "github-actions[bot]"
|
|
77
|
+
echo "Git config successful"
|
|
78
|
+
|
|
79
|
+
- name: Test Git operations (dry run)
|
|
80
|
+
run: |
|
|
81
|
+
echo "Would create tag: v0.2.1"
|
|
82
|
+
echo "Would push to origin"
|
|
83
|
+
# Don't actually push in test
|
|
84
|
+
|
|
85
|
+
- name: Test gem build
|
|
86
|
+
run: |
|
|
87
|
+
gem build pg_multitenant_schemas.gemspec
|
|
88
|
+
ls -la *.gem
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## ๐ Troubleshooting Steps
|
|
92
|
+
|
|
93
|
+
### 1. Check Token Permissions
|
|
94
|
+
```bash
|
|
95
|
+
# In GitHub Actions, this should show the permissions
|
|
96
|
+
echo "GITHUB_TOKEN permissions:"
|
|
97
|
+
echo "Contents: ${{ github.token }}"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 2. Verify Repository Access
|
|
101
|
+
```bash
|
|
102
|
+
# Test if the bot can access the repo
|
|
103
|
+
git ls-remote origin
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 3. Check Branch Protection Rules
|
|
107
|
+
- Go to Settings > Branches
|
|
108
|
+
- Check if `main` branch has protection rules that block force pushes
|
|
109
|
+
- Ensure "Restrict pushes that create files" is not enabled
|
|
110
|
+
|
|
111
|
+
## ๐ฏ Quick Fix Summary
|
|
112
|
+
|
|
113
|
+
1. **โ
Updated workflow permissions** (already done)
|
|
114
|
+
2. **โ
Fixed bot identity** (already done)
|
|
115
|
+
3. **๐ง Check repository settings** (manual step on GitHub.com)
|
|
116
|
+
4. **๐งช Test with manual trigger** (if needed)
|
|
117
|
+
|
|
118
|
+
## ๐ Manual Steps Required
|
|
119
|
+
|
|
120
|
+
1. Go to your repository on GitHub.com
|
|
121
|
+
2. Navigate to Settings > Actions > General
|
|
122
|
+
3. Set "Workflow permissions" to "Read and write permissions"
|
|
123
|
+
4. Enable "Allow GitHub Actions to create and approve pull requests"
|
|
124
|
+
5. Save the settings
|
|
125
|
+
|
|
126
|
+
After making these changes, the release workflow should work properly!
|
|
127
|
+
|
|
128
|
+
## ๐ Testing the Fix
|
|
129
|
+
|
|
130
|
+
After updating repository settings, you can test by:
|
|
131
|
+
|
|
132
|
+
1. **Making a small version bump**
|
|
133
|
+
2. **Pushing to main branch**
|
|
134
|
+
3. **Watching the Actions tab** for successful execution
|
|
135
|
+
|
|
136
|
+
Or create the test workflow above and trigger it manually first.
|