state_sync 0.1.0
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 +7 -0
- data/GITHUB.md +127 -0
- data/GITLAB.md +106 -0
- data/LICENSE +13 -0
- data/README.md +134 -0
- data/lib/state_sync/configuration.rb +23 -0
- data/lib/state_sync/errors.rb +3 -0
- data/lib/state_sync/fetchers/github_fetcher.rb +52 -0
- data/lib/state_sync/fetchers/gitlab_fetcher.rb +44 -0
- data/lib/state_sync/store.rb +63 -0
- data/lib/state_sync/version.rb +3 -0
- data/lib/state_sync.rb +29 -0
- metadata +82 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c0168fcb451c250ca0603039b16d46b17549155e16220022d42659525492c583
|
|
4
|
+
data.tar.gz: 799020c89366e8c271eb64c403004f545fdc0703cf5ac6704c07c372a4676997
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a6afe4b0bf9392cfc4f9335ad283fdf6995b4ffe395274a9e2ecee83a283c0ee358b5db0ffa4f71731e613874b6f395a2a3b586696f53d17c2b8fe94ed673719
|
|
7
|
+
data.tar.gz: 560ddef2c20ed4ff91388e2ede1353c4f2b0b4022478cb7389ec5dc49a28b81dc03b4f68c00381b35fb53c0e07f02da51c7282da4403260e2d0ef4af9dcdefa2
|
data/GITHUB.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# GitHub Setup for state_sync
|
|
2
|
+
|
|
3
|
+
`state_sync` reads YAML files directly from a GitHub repository using the GitHub Contents API.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Public repositories
|
|
8
|
+
|
|
9
|
+
No token is required. You can omit `config.token` entirely.
|
|
10
|
+
However, unauthenticated requests are rate-limited to **60 requests/hour** per IP.
|
|
11
|
+
If you enable `auto_refresh` with a short interval, you will hit this limit quickly.
|
|
12
|
+
It is recommended to always provide a token even for public repos (authenticated limit is 5,000/hour).
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Private repositories
|
|
17
|
+
|
|
18
|
+
A GitHub token with read access to the repository is required.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Creating a token
|
|
23
|
+
|
|
24
|
+
### Option A — Fine-grained Personal Access Token (recommended)
|
|
25
|
+
|
|
26
|
+
Fine-grained tokens let you limit access to specific repositories and specific permissions.
|
|
27
|
+
|
|
28
|
+
1. Go to **GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens**
|
|
29
|
+
2. Click **Generate new token**
|
|
30
|
+
3. Set a name (e.g. `state_sync`) and an expiration
|
|
31
|
+
4. Under **Repository access**, select **Only select repositories** and pick the repo that holds your YAML files
|
|
32
|
+
5. Under **Permissions → Repository permissions**, find **Contents** and set it to **Read-only**
|
|
33
|
+
6. Click **Generate token** and copy it immediately — GitHub will not show it again
|
|
34
|
+
|
|
35
|
+
### Option B — Classic Personal Access Token
|
|
36
|
+
|
|
37
|
+
1. Go to **GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic)**
|
|
38
|
+
2. Click **Generate new token (classic)**
|
|
39
|
+
3. Set a note and expiration
|
|
40
|
+
4. Select the **`repo`** scope (grants full read/write to private repos — use fine-grained tokens if you want narrower access)
|
|
41
|
+
5. Click **Generate token** and copy it
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Storing the token safely
|
|
46
|
+
|
|
47
|
+
Never hardcode a token in your source code. Use an environment variable:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# .env (not committed to git)
|
|
51
|
+
GITHUB_TOKEN=ghp_your_token_here
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Then reference it in your configuration:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
StateSync.configure do |config|
|
|
58
|
+
config.provider = :github
|
|
59
|
+
config.token = ENV["GITHUB_TOKEN"]
|
|
60
|
+
config.repo = "your-org/your-repo"
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
If you use Rails credentials, you can also store it there:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
rails credentials:edit
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
github:
|
|
72
|
+
state_sync_token: ghp_your_token_here
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
config.token = Rails.application.credentials.dig(:github, :state_sync_token)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Rate limits
|
|
82
|
+
|
|
83
|
+
| Authentication | Requests per hour |
|
|
84
|
+
|-----------------------|-------------------|
|
|
85
|
+
| None (public repos) | 60 |
|
|
86
|
+
| Personal Access Token | 5,000 |
|
|
87
|
+
|
|
88
|
+
If you enable `auto_refresh` with `auto_refresh_interval: 60` (every minute) and have 10 files loaded,
|
|
89
|
+
that is 600 requests/hour — well within the authenticated limit but it will exceed the unauthenticated limit.
|
|
90
|
+
Always use a token when `auto_refresh` is enabled.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Examples
|
|
95
|
+
|
|
96
|
+
### Without auto refresh
|
|
97
|
+
|
|
98
|
+
Data is fetched once when the server starts. It does not change until the server restarts.
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
StateSync.configure do |config|
|
|
102
|
+
config.provider = :github
|
|
103
|
+
config.repo = "your-org/your-repo"
|
|
104
|
+
config.token = ENV["GITHUB_TOKEN"]
|
|
105
|
+
config.auto_refresh = false
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
customers = StateSync.load("config/customers.yml")
|
|
109
|
+
customers["customer_ids"] # => [1001, 1002, 1003]
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### With auto refresh
|
|
113
|
+
|
|
114
|
+
Data is fetched at startup and a background thread keeps it updated at the configured interval.
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
StateSync.configure do |config|
|
|
118
|
+
config.provider = :github
|
|
119
|
+
config.repo = "your-org/your-repo"
|
|
120
|
+
config.token = ENV["GITHUB_TOKEN"]
|
|
121
|
+
config.auto_refresh = true
|
|
122
|
+
config.auto_refresh_interval = 300 # seconds
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
customers = StateSync.load("config/customers.yml")
|
|
126
|
+
customers["customer_ids"] # => always current
|
|
127
|
+
```
|
data/GITLAB.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# GitLab Setup for state_sync
|
|
2
|
+
|
|
3
|
+
`state_sync` reads YAML files directly from a GitLab project using the GitLab Repository Files API.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Public projects
|
|
8
|
+
|
|
9
|
+
No token is required for public projects. You can omit `config.token` entirely.
|
|
10
|
+
A token is still recommended to avoid hitting rate limits, especially when `auto_refresh` is enabled.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Private projects
|
|
15
|
+
|
|
16
|
+
A GitLab personal access token with `read_repository` scope is required.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Creating a token
|
|
21
|
+
|
|
22
|
+
1. Go to **GitLab → Edit profile → Access tokens**
|
|
23
|
+
2. Click **Add new token**
|
|
24
|
+
3. Give it a name (e.g. `state_sync`) and set an expiry date
|
|
25
|
+
4. Under **Select scopes**, check **`read_repository`**
|
|
26
|
+
5. Click **Create personal access token** and copy it immediately — GitLab will not show it again
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Storing the token safely
|
|
31
|
+
|
|
32
|
+
Never hardcode a token in your source code. Use an environment variable:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# .env (not committed to git)
|
|
36
|
+
GITLAB_TOKEN=glpat_your_token_here
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Then reference it in your configuration:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
StateSync.configure do |config|
|
|
43
|
+
config.provider = :gitlab
|
|
44
|
+
config.token = ENV["GITLAB_TOKEN"]
|
|
45
|
+
config.repo = "your-org/your-repo"
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
If you use Rails credentials, you can also store it there:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
rails credentials:edit
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```yaml
|
|
56
|
+
gitlab:
|
|
57
|
+
state_sync_token: glpat_your_token_here
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
config.token = Rails.application.credentials.dig(:gitlab, :state_sync_token)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Rate limits
|
|
67
|
+
|
|
68
|
+
GitLab rate limits vary by plan and instance. Files over 10 MB are limited to 5 requests/minute.
|
|
69
|
+
Always use a token when `auto_refresh` is enabled.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Examples
|
|
74
|
+
|
|
75
|
+
### Without auto refresh
|
|
76
|
+
|
|
77
|
+
Data is fetched once when the server starts. It does not change until the server restarts.
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
StateSync.configure do |config|
|
|
81
|
+
config.provider = :gitlab
|
|
82
|
+
config.repo = "your-org/your-repo"
|
|
83
|
+
config.token = ENV["GITLAB_TOKEN"]
|
|
84
|
+
config.auto_refresh = false
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
customers = StateSync.load("config/customers.yml")
|
|
88
|
+
customers["customer_ids"] # => [1001, 1002, 1003]
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### With auto refresh
|
|
92
|
+
|
|
93
|
+
Data is fetched at startup and a background thread keeps it updated at the configured interval.
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
StateSync.configure do |config|
|
|
97
|
+
config.provider = :gitlab
|
|
98
|
+
config.repo = "your-org/your-repo"
|
|
99
|
+
config.token = ENV["GITLAB_TOKEN"]
|
|
100
|
+
config.auto_refresh = true
|
|
101
|
+
config.auto_refresh_interval = 300 # seconds
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
customers = StateSync.load("config/customers.yml")
|
|
105
|
+
customers["customer_ids"] # => always current
|
|
106
|
+
```
|
data/LICENSE
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
2
|
+
Version 2, December 2004
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
|
5
|
+
|
|
6
|
+
Everyone is permitted to copy and distribute verbatim or modified
|
|
7
|
+
copies of this license document, and changing it is allowed as long
|
|
8
|
+
as the name is changed.
|
|
9
|
+
|
|
10
|
+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
11
|
+
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
|
12
|
+
|
|
13
|
+
0. You just DO WHAT THE FUCK YOU WANT TO.
|
data/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# state_sync
|
|
2
|
+
|
|
3
|
+
A Ruby gem for fetching YAML-based feature flags and configuration from a GitHub or GitLab repository.
|
|
4
|
+
Values are loaded at startup and can optionally be kept fresh in the background.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
Add to your Gemfile:
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
gem "state_sync"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Or install directly:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
gem install state_sync
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Configuration
|
|
23
|
+
|
|
24
|
+
Token setup: [GITHUB.md](GITHUB.md) | [GITLAB.md](GITLAB.md)
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
# config/initializers/state_sync.rb
|
|
28
|
+
StateSync.configure do |config|
|
|
29
|
+
config.provider = :github # :github or :gitlab
|
|
30
|
+
config.repo = "your-org/your-repo" # "owner/repo" on GitHub or GitLab
|
|
31
|
+
config.token = ENV["GITHUB_TOKEN"] # optional for public repos
|
|
32
|
+
config.auto_refresh = false # true or false (default: false)
|
|
33
|
+
config.auto_refresh_interval = 300 # seconds, only required when auto_refresh is true
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
# Load a YAML file — fetches immediately on this line
|
|
43
|
+
CUSTOMERS = StateSync.load("config/customers.yml")
|
|
44
|
+
|
|
45
|
+
# Access data
|
|
46
|
+
CUSTOMERS.data # => {"customer_ids" => [1001, 1002, 1003], "feature_x" => true}
|
|
47
|
+
CUSTOMERS["customer_ids"] # => [1001, 1002, 1003]
|
|
48
|
+
CUSTOMERS["feature_x"] # => true
|
|
49
|
+
|
|
50
|
+
# Force a manual refresh at any time
|
|
51
|
+
CUSTOMERS.reload!
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Example YAML file
|
|
55
|
+
|
|
56
|
+
```yaml
|
|
57
|
+
customer_ids:
|
|
58
|
+
- 1001
|
|
59
|
+
- 1002
|
|
60
|
+
- 1003
|
|
61
|
+
|
|
62
|
+
feature_x: true
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Loading multiple files
|
|
66
|
+
|
|
67
|
+
You can load as many files as you need — each gets its own store with independent data and refresh cycle. Define them in your initializer and use them anywhere in your app:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
# config/initializers/state_sync.rb
|
|
71
|
+
StateSync.configure do |config|
|
|
72
|
+
config.provider = :github
|
|
73
|
+
config.repo = "your-org/your-repo" # "owner/repo" on GitHub or GitLab
|
|
74
|
+
config.token = ENV["GITHUB_TOKEN"]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
CUSTOMERS = StateSync.load("config/customers.yml")
|
|
78
|
+
FEATURE_FLAGS = StateSync.load("config/feature_flags.yml")
|
|
79
|
+
PAYMENT_METHODS = StateSync.load("config/payment_methods.yml")
|
|
80
|
+
PAYMENT_LIMITS = StateSync.load("config/payment_limits.yml")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
# Use anywhere in your app
|
|
85
|
+
CUSTOMERS["customer_ids"]
|
|
86
|
+
FEATURE_FLAGS["new_checkout_flow"]
|
|
87
|
+
PAYMENT_METHODS["enabled"]
|
|
88
|
+
PAYMENT_LIMITS["daily_limit"]
|
|
89
|
+
```
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Error handling
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
begin
|
|
96
|
+
flags = StateSync.load("config/flags.yml")
|
|
97
|
+
rescue StateSync::ConfigurationError => e
|
|
98
|
+
# Missing or invalid configuration
|
|
99
|
+
rescue StateSync::FetchError => e
|
|
100
|
+
# Network error, bad token, file not found, etc.
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Try it in IRB
|
|
107
|
+
|
|
108
|
+
First install the gem:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
gem install state_sync
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Then start IRB and require the gem:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
irb
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
require "state_sync"
|
|
122
|
+
|
|
123
|
+
StateSync.configure do |config|
|
|
124
|
+
config.provider = :github
|
|
125
|
+
config.repo = "spmarisa/state_sync_data" # public repo, no token needed
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
customers = StateSync.load("allowed_customers.yml")
|
|
129
|
+
customers.data
|
|
130
|
+
customers["customer_ids"]
|
|
131
|
+
|
|
132
|
+
# Force a refresh
|
|
133
|
+
customers.reload!
|
|
134
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class StateSync::Configuration
|
|
2
|
+
PROVIDERS = %i[github gitlab].freeze
|
|
3
|
+
|
|
4
|
+
attr_accessor :provider, :repo, :token, :auto_refresh, :auto_refresh_interval
|
|
5
|
+
|
|
6
|
+
def initialize
|
|
7
|
+
@provider = :github
|
|
8
|
+
@auto_refresh = false
|
|
9
|
+
@auto_refresh_interval = 300
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def validate!
|
|
13
|
+
unless PROVIDERS.include?(provider)
|
|
14
|
+
raise StateSync::ConfigurationError, "provider must be :github or :gitlab"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
raise StateSync::ConfigurationError, "repo must be set (e.g. \"owner/repo\")" if repo.nil? || repo.strip.empty?
|
|
18
|
+
|
|
19
|
+
if auto_refresh && (auto_refresh_interval.nil? || auto_refresh_interval <= 0)
|
|
20
|
+
raise StateSync::ConfigurationError, "auto_refresh_interval must be a positive number of seconds when auto_refresh is true"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "uri"
|
|
3
|
+
require "json"
|
|
4
|
+
require "base64"
|
|
5
|
+
|
|
6
|
+
class StateSync::GithubFetcher
|
|
7
|
+
API_BASE = "https://api.github.com"
|
|
8
|
+
|
|
9
|
+
def initialize(config)
|
|
10
|
+
@config = config
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Fetches raw file content (string) from GitHub.
|
|
14
|
+
# No ?ref= param — GitHub uses the repo's default branch automatically.
|
|
15
|
+
def fetch(path)
|
|
16
|
+
uri = URI("#{API_BASE}/repos/#{@config.repo}/contents/#{path}")
|
|
17
|
+
|
|
18
|
+
request = Net::HTTP::Get.new(uri)
|
|
19
|
+
request["Accept"] = "application/vnd.github+json"
|
|
20
|
+
request["X-GitHub-Api-Version"] = "2022-11-28"
|
|
21
|
+
request["Authorization"] = "Bearer #{@config.token}" if @config.token
|
|
22
|
+
|
|
23
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
|
24
|
+
http.request(request)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
handle_response(response, path)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def handle_response(response, path)
|
|
33
|
+
case response.code.to_i
|
|
34
|
+
when 200
|
|
35
|
+
json = JSON.parse(response.body)
|
|
36
|
+
|
|
37
|
+
if json["encoding"] == "base64"
|
|
38
|
+
Base64.decode64(json["content"])
|
|
39
|
+
else
|
|
40
|
+
raise StateSync::FetchError, "Unexpected encoding from GitHub: #{json["encoding"]}"
|
|
41
|
+
end
|
|
42
|
+
when 401
|
|
43
|
+
raise StateSync::FetchError, "GitHub authentication failed — check your github_token."
|
|
44
|
+
when 403
|
|
45
|
+
raise StateSync::FetchError, "GitHub access forbidden — ensure your token has 'Contents' read permission."
|
|
46
|
+
when 404
|
|
47
|
+
raise StateSync::FetchError, "File not found: '#{path}' in repo '#{@config.repo}'. Check the path and that the repo exists."
|
|
48
|
+
else
|
|
49
|
+
raise StateSync::FetchError, "GitHub API returned #{response.code}: #{response.body}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "uri"
|
|
3
|
+
|
|
4
|
+
class StateSync::GitlabFetcher
|
|
5
|
+
API_BASE = "https://gitlab.com/api/v4"
|
|
6
|
+
|
|
7
|
+
def initialize(config)
|
|
8
|
+
@config = config
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Fetches raw file content from GitLab using the repository files raw endpoint.
|
|
12
|
+
# ref=HEAD always resolves to the project's default branch.
|
|
13
|
+
def fetch(path)
|
|
14
|
+
encoded_repo = URI.encode_www_form_component(@config.repo)
|
|
15
|
+
encoded_path = URI.encode_www_form_component(path)
|
|
16
|
+
uri = URI("#{API_BASE}/projects/#{encoded_repo}/repository/files/#{encoded_path}/raw?ref=HEAD")
|
|
17
|
+
|
|
18
|
+
request = Net::HTTP::Get.new(uri)
|
|
19
|
+
request["PRIVATE-TOKEN"] = @config.token if @config.token
|
|
20
|
+
|
|
21
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
|
22
|
+
http.request(request)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
handle_response(response, path)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def handle_response(response, path)
|
|
31
|
+
case response.code.to_i
|
|
32
|
+
when 200
|
|
33
|
+
response.body
|
|
34
|
+
when 401
|
|
35
|
+
raise StateSync::FetchError, "GitLab authentication failed — check your gitlab_token."
|
|
36
|
+
when 403
|
|
37
|
+
raise StateSync::FetchError, "GitLab access forbidden — ensure your token has 'read_repository' scope."
|
|
38
|
+
when 404
|
|
39
|
+
raise StateSync::FetchError, "File not found: '#{path}' in repo '#{@config.repo}'. Check the path and that the project exists."
|
|
40
|
+
else
|
|
41
|
+
raise StateSync::FetchError, "GitLab API returned #{response.code}: #{response.body}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
|
|
3
|
+
# Holds the parsed contents of a single YAML file fetched from GitHub or GitLab.
|
|
4
|
+
# On initialization it fetches the file immediately. If auto_refresh is
|
|
5
|
+
# enabled a background thread keeps the data current at the configured interval.
|
|
6
|
+
class StateSync::Store
|
|
7
|
+
def initialize(path)
|
|
8
|
+
@path = path
|
|
9
|
+
@fetcher = fetcher_for(StateSync.configuration)
|
|
10
|
+
@mutex = Mutex.new
|
|
11
|
+
|
|
12
|
+
fetch_and_cache
|
|
13
|
+
|
|
14
|
+
start_background_refresh if StateSync.configuration.auto_refresh
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Returns the parsed YAML data (Hash or Array depending on file content).
|
|
18
|
+
def data
|
|
19
|
+
@mutex.synchronize { @data }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Shorthand key access when the YAML root is a Hash.
|
|
23
|
+
def [](key)
|
|
24
|
+
@mutex.synchronize { @data[key] }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Force an immediate re-fetch from the configured provider.
|
|
28
|
+
def reload!
|
|
29
|
+
fetch_and_cache
|
|
30
|
+
self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def fetcher_for(config)
|
|
36
|
+
case config.provider
|
|
37
|
+
when :github then StateSync::GithubFetcher.new(config)
|
|
38
|
+
when :gitlab then StateSync::GitlabFetcher.new(config)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def fetch_and_cache
|
|
43
|
+
content = @fetcher.fetch(@path)
|
|
44
|
+
parsed = YAML.safe_load(content)
|
|
45
|
+
@mutex.synchronize { @data = parsed }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def start_background_refresh
|
|
49
|
+
interval = StateSync.configuration.auto_refresh_interval
|
|
50
|
+
|
|
51
|
+
Thread.new do
|
|
52
|
+
Thread.current.daemon = true
|
|
53
|
+
loop do
|
|
54
|
+
sleep interval
|
|
55
|
+
begin
|
|
56
|
+
fetch_and_cache
|
|
57
|
+
rescue => e
|
|
58
|
+
warn "[StateSync] Failed to refresh '#{@path}': #{e.message}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
data/lib/state_sync.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require "state_sync/version"
|
|
2
|
+
require "state_sync/errors"
|
|
3
|
+
require "state_sync/configuration"
|
|
4
|
+
require "state_sync/store"
|
|
5
|
+
Dir[File.join(__dir__, "state_sync/fetchers/*.rb")].each { |f| require f }
|
|
6
|
+
|
|
7
|
+
module StateSync
|
|
8
|
+
class << self
|
|
9
|
+
def configure
|
|
10
|
+
yield configuration
|
|
11
|
+
configuration.validate!
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def configuration
|
|
15
|
+
@configuration ||= Configuration.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Loads a YAML file from the configured provider (GitHub or GitLab) and returns a Store.
|
|
19
|
+
# The file is fetched immediately; if auto_refresh is enabled a background
|
|
20
|
+
# thread keeps it updated at the configured interval.
|
|
21
|
+
#
|
|
22
|
+
# Example:
|
|
23
|
+
# customers = StateSync.load("config/customers.yml")
|
|
24
|
+
# customers["allowed_ids"] # => [1, 2, 3]
|
|
25
|
+
def load(path)
|
|
26
|
+
Store.new(path)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: state_sync
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Phaneendra Marisa
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-04-01 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rspec
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '3.13'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '3.13'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: webmock
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.23'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.23'
|
|
41
|
+
description:
|
|
42
|
+
email:
|
|
43
|
+
- phaneendra.marisa@gmail.com
|
|
44
|
+
executables: []
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files: []
|
|
47
|
+
files:
|
|
48
|
+
- GITHUB.md
|
|
49
|
+
- GITLAB.md
|
|
50
|
+
- LICENSE
|
|
51
|
+
- README.md
|
|
52
|
+
- lib/state_sync.rb
|
|
53
|
+
- lib/state_sync/configuration.rb
|
|
54
|
+
- lib/state_sync/errors.rb
|
|
55
|
+
- lib/state_sync/fetchers/github_fetcher.rb
|
|
56
|
+
- lib/state_sync/fetchers/gitlab_fetcher.rb
|
|
57
|
+
- lib/state_sync/store.rb
|
|
58
|
+
- lib/state_sync/version.rb
|
|
59
|
+
homepage: https://github.com/spmarisa/state_sync
|
|
60
|
+
licenses:
|
|
61
|
+
- WTFPL
|
|
62
|
+
metadata: {}
|
|
63
|
+
post_install_message:
|
|
64
|
+
rdoc_options: []
|
|
65
|
+
require_paths:
|
|
66
|
+
- lib
|
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
68
|
+
requirements:
|
|
69
|
+
- - ">="
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: 2.5.0
|
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
73
|
+
requirements:
|
|
74
|
+
- - ">="
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: '0'
|
|
77
|
+
requirements: []
|
|
78
|
+
rubygems_version: 3.0.3.1
|
|
79
|
+
signing_key:
|
|
80
|
+
specification_version: 4
|
|
81
|
+
summary: Fetch and auto-refresh YAML-based feature flags and config from a Git repository.
|
|
82
|
+
test_files: []
|