Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: CI

on:
pull_request:
push:
branches: [ master ]

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
name: Build (JDK 21 + Ruby 3.2.8)
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

- name: Set up JDK 21
uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4.9.1
with:
distribution: temurin
java-version: '21'

- name: Set up Ruby
uses: ruby/setup-ruby@651ff0d62c5c79bed6d53ec518387aae20e72c1a # v1.329.0
with:
ruby-version: '3.2.8'
bundler-cache: false

- name: Cache Gradle
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle', 'gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: gradle-${{ runner.os }}-

- name: Build
run: ./gradlew build --no-daemon --stacktrace

- name: Upload test reports
if: ${{ !cancelled() }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: test-reports
path: |
build/reports/tests/
build/test-results/
src/main/ruby/**/tmp/
if-no-files-found: ignore
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/.gradle/
/.idea/
/build/
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ruby 3.2.8
175 changes: 175 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# CLAUDE.md

Guidance for Claude agents working in this repository.

## Project

OPA (On-Premise Agent) extensions. Custom Java classes compiled against the
agent's Extension API and loaded by the agent at runtime. Sources live under
`src/main/java/com/mycompany/onprem/`.

## Extension API & test-mock JARs

The compile and runtime classpaths depend on two JARs shipped with the agent
distribution and placed in `extension-api/`:

- `extension-api-<version>.jar` — the Extension API (compile dependency).
- `extension-test-<version>.jar` — a mock/harness used for writing unit tests
against the API (runtime dependency).

Both JARs **must come from the same agent distribution** (identical version
suffix). The Gradle `validateExtensionJars` task enforces this and fails the
build if the directory is missing, a JAR is absent, versions are mixed, or
multiple versions of either JAR are present.

## Authoritative documentation — read before coding

The Gradle build extracts the README from each JAR into `extension-api/` on
every build:

- **`extension-api/README-extension-api.md`** — the contract for writing an
extension: available interfaces, lifecycle, injection points, allowed
packages, and usage examples. Any new extension class under
`src/main/java/com/mycompany/onprem/` **must** follow this document.
- **`extension-api/README-extension-test.md`** — the contract for writing
tests against a mocked Extension API: available fakes/builders, how to wire
the mock runtime, and example test patterns. Any new test under `src/test/`
**must** follow this document.

These files are the single source of truth for *how* to use the API and *how*
to test against it. They are regenerated from the JARs on every build and
therefore always match the version currently on the classpath — prefer them
over any external notes or stale examples.

### Required workflow for Claude agents

Before writing or modifying an extension or its tests:

1. Run `./gradlew validateExtensionJars extractExtensionReadmes` (or any build
task — both run as part of `compileJava`) to ensure the READMEs are
present and up to date.
2. Read `extension-api/README-extension-api.md` end-to-end when touching
production code under `src/main/java/`.
3. Read `extension-api/README-extension-test.md` end-to-end when touching or
adding tests under `src/test/`.
4. Follow the patterns and constraints documented there. Do not invent
alternative API usage, do not mock the Extension API by hand — use the
facilities provided by `extension-test-*.jar` as described in its README.

If a README is missing, the JARs have not been copied from the agent
distribution yet. Ask the user to copy them rather than guessing at the API.

## Workato custom connectors (Ruby)

Each on-prem extension has a companion Workato custom connector (a Ruby
`connector.rb` file) under `src/main/ruby/<name>/`. The test harness that
exercises each connector lives under `src/test/ruby/<name>/` as a
**self-contained Workato SDK project** (Gemfile + settings.yaml + specs + VCR
cassettes). The connectors are what Workato recipes talk to; the agent
extensions are what the connectors talk to.

Current connectors (deliverable → test project):

- `src/main/ruby/security/connector.rb` → `src/test/ruby/security/` → calls `SecurityExtension`
- `src/main/ruby/ntlm/connector.rb` → `src/test/ruby/ntlm/` → calls `NtlmExtension`

### When adding or editing a connector

- Keep each connector in its own subdirectory under `src/main/ruby/<name>/`.
The file **must be named `connector.rb`** — the Workato SDK does not accept
any other name.
- The matching `src/test/ruby/<name>/` directory holds `settings.yaml`
(connection fields the tests load) and `spec/connector_spec.rb`. The shared
`Gemfile` (`workato-connector-sdk` + rspec + vcr + webmock) and
`spec_helper.rb` live one level up at `src/test/ruby/`. VCR cassettes are
checked in under `src/test/resources/tape_library/<name>/`.
- Specs reference the connector via an absolute path pinned at the top of
`connector_spec.rb` as
`CONNECTOR_PATH = File.expand_path('../../../../main/ruby/<name>/connector.rb', __dir__).freeze`
— the relative hop crosses `test → main`, so adjust it carefully if you
nest spec files deeper.
- Any HTTP the connector performs must be stubbed via VCR cassettes under
`src/test/resources/tape_library/<name>/`. Tests must not touch the network —
the harness is configured with `record: :none`.
- When you add an action that calls a new URL, add a new cassette YAML under
`src/test/resources/tape_library/<name>/` (minimal format: see the existing files).
- **One Gradle task per spec file.** Today we have `:securityRspec` ↔
`src/test/ruby/security/spec/connector_spec.rb` and `:ntlmRspec` ↔
`src/test/ruby/ntlm/spec/connector_spec.rb`. Any new `*_spec.rb` must get
its own `:<name>Rspec` task in `build.gradle` (model it on the existing
two — same `rubyBundleInstall` dependency, `workingDir` set to the
per-connector dir, `commandLine rspecCommand(project.findProperty('spec'))`).
This keeps every spec individually runnable from both CLI and the IDE
(one Gradle run-config per task), and it makes the `-Pspec=…` filter
behave predictably: it narrows an already-scoped task instead of fanning
out across every spec in the project.
- **Mandatory header comment on every spec.** The first non-shebang block
of each `*_spec.rb` must spell out how to run it from Gradle *and* from
the IDE (both Ruby-plugin and Community/Gradle-delegate paths). Copy the
template from `src/test/ruby/security/spec/connector_spec.rb` and swap
the task name + an example-description substring. The comment is the
single hand-off between a reader of the spec and someone trying to
reproduce one failing example — do not skip it.

## Running tests

- **`./gradlew test`** — runs Java JUnit tests (`:test`) and then both Ruby
RSpec suites (`:securityRspec`, `:ntlmRspec`) via `finalizedBy`. Both report
per-test output; both failures surface in a single build.
- **`./gradlew check`** — same as above plus any other verification tasks.
- **`./gradlew :test`** — Java only.
- **`./gradlew testRubyConnectors`** — both Ruby suites only.
- **`./gradlew :securityRspec`** / **`./gradlew :ntlmRspec`** — one Ruby suite.

### Writing Java tests

- Tests live under `src/test/java/com/mycompany/onprem/`.
- JUnit 5 Jupiter. The Extension API mock harness is on the test classpath via
`extension-test-<version>.jar` → see `extension-api/README-extension-test.md`
for the `AgentExtensionHarness` DSL. Do not hand-roll a mock of the API.
- Real HTTP in a test is fine when it's a tiny in-process server (see
`NtlmExtensionTest` → `com.sun.net.httpserver.HttpServer` on `127.0.0.1:0`).

### Writing Ruby tests

- Test framework: RSpec + VCR + WebMock (all declared in the shared
`src/test/ruby/Gemfile`; `Gemfile.lock` and `spec_helper.rb` live next to
it, each per-connector `spec/spec_helper.rb` is a one-line
`require_relative` wrapper).
- Pattern per spec:
```ruby
CONNECTOR_PATH = File.expand_path('../../../../main/ruby/<name>/connector.rb', __dir__).freeze

let(:settings) { Workato::Connector::Sdk::Settings.from_default_file }
let(:connector) { Workato::Connector::Sdk::Connector.from_file(CONNECTOR_PATH, settings) }
```
Then invoke:
- `connector.actions.<name>.execute(settings, input_hash)`
- `connector.methods.<name>(args...)`
- `connector.test(settings)` for the connection test lambda.
- Wrap HTTP-touching tests in `VCR.use_cassette('name') do ... end`. The
cassette file is `src/test/resources/tape_library/<connector>/<name>.yml`
(the shared `spec_helper.rb` resolves the `<connector>` subdir from the
CWD basename).
- No real network: `spec_helper.rb` sets `record: :none`, so an unmocked
request fails loudly.
- To iterate locally without going through Gradle: `cd src/test/ruby/<name>
&& bundle install && bundle exec rspec`.

## Toolchain

- Java 21, Gradle 8.10.2.
- Ruby 3.2.8 (pinned via `.tool-versions` for asdf). The Workato SDK gem
(`workato-connector-sdk`) is self-contained — no Workato cloud / account
needed to run tests.
- The Gradle `bundleBin` helper locates `bundle` in this order:
`$BUNDLE_BIN` → `~/.asdf/shims/bundle` → `bundle` on PATH. If you're on a
CI runner that provisions Ruby a different way, set `BUNDLE_BIN` to the
resulting bundler executable.

## Build

- `./gradlew build` — full build; triggers JAR validation, README extraction,
Java tests, and Ruby tests.
- `./gradlew validateExtensionJars` — JAR presence/version check only.
- `./gradlew extractExtensionReadmes` — refresh the two README files.
Loading
Loading