Skip to content

feat(datasource-pylon): foundation β€” gem, config & resilient client - #341

Open
christophebrun-forest wants to merge 1 commit into
feat/datasource-pylonfrom
feature/ext-5-story-1-foundation-gem-config-resilient-client
Open

feat(datasource-pylon): foundation β€” gem, config & resilient client#341
christophebrun-forest wants to merge 1 commit into
feat/datasource-pylonfrom
feature/ext-5-story-1-foundation-gem-config-resilient-client

Conversation

@christophebrun-forest

@christophebrun-forest christophebrun-forest commented Aug 4, 2026

Copy link
Copy Markdown
Member

Story 1 of the Pylon datasource β€” EXT-5. Targets the integration branch feat/datasource-pylon (EXT-4), not main.

What this adds

A new forest_admin_datasource_pylon gem skeleton with an authenticated, rate-limit-aware HTTP client:

  • Entry point β€” Zeitwerk for_gem autoloading, typed error hierarchy (Error / ConfigurationError / UnsupportedOperatorError / APIError), configurable logger with a Rails.logger fallback. APIError carries the HTTP status and the parsed body so the smart actions in Story 8 can surface Pylon's own validation message.
  • Configuration β€” validated api_key, base URL https://api.usepylon.com (Pylon paths are unversioned), configurable timeouts and retry budget.
  • Client β€” Faraday with Authorization: Bearer, JSON in/out, 429 retry with exponential backoff, GET /me health check, plus the envelope-unwrapping and error-mapping helpers the later stories build on.

Faraday middleware order β€” deliberate divergence from the Mambu Payments gem

forest_admin_datasource_mambu_payments registers retry before raise_error, which means 429s are never actually retried: raise_error sits inside, so it raises Faraday::TooManyRequestsError, which is not in faraday-retry's default exceptions β€” the middleware never observes a response and retry_statuses silently does nothing.

This gem inverts the order: raise_error outside the JSON parser (errors carry an already-parsed body) and retry innermost, where it sees raw statuses. Two specs pin the behaviour β€” one 429 then 200 issues 2 requests; a persistent 429 issues 3 and raises APIError with status: 429.

Non-idempotent verbs are retried only on 429, where Pylon rejected the request before processing it β€” a 502 on POST /issues may well have created the issue. Implemented with retry_if rather than methods, because faraday-retry ORs the two conditions and retry_if can therefore only widen, never restrict.

Worth a separate fix on the Mambu gem.

Monorepo wiring

forest_admin_datasource_pylon added to the lint matrix, test matrix and coverage file list in build.yml, plus the needed .rubocop.yml excludes.

.releaserc.js is intentionally untouched, so no incomplete gem can reach RubyGems β€” the deploy job only runs on push to main/beta, so no story PR into the integration branch can publish. The exact 3-line patch is recorded as a comment on EXT-13 (Story 9), which owns "add gem to the release/publish workflow". Consequence: the Pylon VERSION stays at 1.36.2 and will drift from main until Story 9 realigns it.

Test plan

  • BUNDLE_GEMFILE=Gemfile-test bundle exec rspec β†’ 29 examples, 0 failures, coverage 96.97% (threshold 90)
  • bundle exec rubocop over the whole repo β†’ 786 files, no offenses
  • Zeitwerk autoloading verified by requiring the gem standalone and resolving Client / Configuration

πŸ€– Generated with Claude Code

Note

Add forest_admin_datasource_pylon gem with configuration and resilient HTTP client

  • Introduces a new Ruby gem package at packages/forest_admin_datasource_pylon with a gemspec, Zeitwerk autoloading, and a structured error hierarchy (Error, ConfigurationError, APIError).
  • Adds a Configuration class with defaults (base URL https://api.usepylon.com, 5s open timeout, 30s timeout, 3 retries) that raises ConfigurationError on missing api_key.
  • Adds a Client class wrapping Faraday with automatic JSON handling, Bearer auth, and retry logic for transient errors (429, 502, 503, 504), including retrying 429 on non-idempotent methods.
  • Exposes a #me endpoint on Client to validate credentials, returning parsed org data or raising APIError with HTTP status and body.
  • CI is wired to lint, test, and collect coverage for the new package.
πŸ“Š Macroscope summarized 8d94a91. 12 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

πŸ—‚οΈ Filtered Issues

No issues evaluated.

Story 1 of the Pylon datasource (EXT-5).

Adds the forest_admin_datasource_pylon gem skeleton: Zeitwerk autoloading,
typed error hierarchy with an APIError carrying HTTP status and parsed body,
configurable logger, Configuration with api_key validation, and a Faraday
client authenticating with a Bearer token plus a GET /me health check.

The Faraday middleware order is deliberate and differs from the Mambu
Payments gem: raise_error sits outside the JSON parser so errors carry an
already-parsed body, and retry sits innermost so it can observe raw statuses.
Behind raise_error the retry middleware never sees a 429 and retry_statuses
silently does nothing. Non-idempotent verbs are only retried on 429, where
Pylon rejected the request before processing it.

Wires the package into the CI lint, test and coverage jobs. The
semantic-release publish pipeline is intentionally left untouched until
Story 9, so an incomplete gem is never pushed to RubyGems.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@linear-code

linear-code Bot commented Aug 4, 2026

Copy link
Copy Markdown

EXT-5

@qltysh

qltysh Bot commented Aug 4, 2026

Copy link
Copy Markdown

4 new issues

Tool Category Rule Count
qlty Duplication Found 17 lines of identical code in 3 locations (mass = 76) 1
qlty Duplication Found 45 lines of similar code in 2 locations (mass = 143) 1
qlty Structure Function with high complexity (count = 5): error_message 1
qlty Structure Function with many parameters (count = 6): initialize 1

gem 'rspec', '~> 3.0'
gem 'simplecov', '~> 0.22', require: false
gem 'webmock', '~> 3.0'
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 17 lines of identical code in 3 locations (mass = 76) [qlty:identical-code]

Logger.new($stderr).tap { |l| l.progname = 'forest_admin_datasource_pylon' }
end
end
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 45 lines of similar code in 2 locations (mass = 143) [qlty:similar-code]

join_errors(parsed['errors'])
message = parsed.to_json if message.to_s.empty?
message = "#{message} (request_id: #{parsed["request_id"]})" if parsed['request_id']
message.to_s[0, 500]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function with high complexity (count = 5): error_message [qlty:function-complexity]


attr_reader :api_key, :base_url, :open_timeout, :timeout, :max_retries, :retry_interval

def initialize(api_key:, base_url: nil, open_timeout: 5, timeout: 30, max_retries: 3, retry_interval: 0.5)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function with many parameters (count = 6): initialize [qlty:function-parameters]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 Medium

ruby-version: ["3.4"]

The Send coverage step references reports/3.4-forest_admin_datasource_pylon/coverage.json, but coverage artifacts are uploaded only for ruby-version == '4.0', so the artifact is named 4.0-forest_admin_datasource_pylon. The 3.4 path does not exist, so the Pylon coverage file is missing and the qltysh/qlty-action/coverage step receives a non-existent input. The coverage job matrix uses 3.4, which mismatches the 4.0 upload condition. Either use 4.0 in the files path for Pylon or align the upload and coverage job matrices to the same version.

πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
In file @.github/workflows/build.yml around line 133:

The `Send coverage` step references `reports/3.4-forest_admin_datasource_pylon/coverage.json`, but coverage artifacts are uploaded only for `ruby-version == '4.0'`, so the artifact is named `4.0-forest_admin_datasource_pylon`. The `3.4` path does not exist, so the Pylon coverage file is missing and the `qltysh/qlty-action/coverage` step receives a non-existent input. The coverage job matrix uses `3.4`, which mismatches the `4.0` upload condition. Either use `4.0` in the `files` path for Pylon or align the upload and coverage job matrices to the same version.

Comment on lines +32 to +33
spec.add_dependency 'activesupport', '>= 6.1'
spec.add_dependency 'faraday', '~> 2.0'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High forest_admin_datasource_pylon/forest_admin_datasource_pylon.gemspec:32

The gemspec omits forest_admin_datasource_toolkit from its runtime dependencies, so installing forest_admin_datasource_pylon does not pull in the toolkit. When users require 'forest_admin_datasource_pylon', the gem fails with LoadError because the toolkit gem is missing. Add forest_admin_datasource_toolkit as a runtime dependency via spec.add_dependency.

  spec.add_dependency 'forest_admin_datasource_toolkit'
+  spec.add_dependency 'activesupport', '>= 6.1'
πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/forest_admin_datasource_pylon/forest_admin_datasource_pylon.gemspec around lines 32-33:

The gemspec omits `forest_admin_datasource_toolkit` from its runtime dependencies, so installing `forest_admin_datasource_pylon` does not pull in the toolkit. When users `require 'forest_admin_datasource_pylon'`, the gem fails with `LoadError` because the toolkit gem is missing. Add `forest_admin_datasource_toolkit` as a runtime dependency via `spec.add_dependency`.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant