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
30 changes: 29 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.0] - 2026-04-16

### Added

- Support for `client.services.session(uuid).logs(recent: true)` for finished-session log retrieval.
- `OctaSpace::ProvisionRejectedError` for MR create requests that are transport-successful but rejected by the API payload contract.
- `OctaSpace::PayloadHelpers` with targeted helpers for stringified app port lists and marketplace bandwidth normalization.
- Diagnostics preset and manual runner support for recent session logs.
- Live-shaped fixtures for recent sessions and render marketplace responses.

### Changed

- `services.mr.list`, `services.render.list`, and `services.vpn.list` are now documented and tested as marketplace catalog endpoints rather than session collections.
- `services.mr.create` now forwards optional `organization_id` and `project_id`.
- Playground Services now presents marketplace-oriented summaries with normalized bandwidth display.
- Playground and README copy now reflect live API quirks for app ports, recent sessions, and finished-session logs.
- Publishing guidance now uses an explicit manual release flow, and the local `rake release` helper is disabled to avoid accidental RubyGems publication.

### Fixed

- Corrected the Ruby public API gap for recent finished-session logs.
- Corrected MR create handling for `HTTP 200` batch-style rejection responses.
- Corrected fixtures and tests that modeled `/services/mr` and `/services/vpn` as session lists.
- Corrected logs fixtures and tests to match the live `{system, container}` payload shape.
- Corrected the playground Apps page so port counts work when the live API returns stringified JSON arrays.
- Corrected test coverage for `sessions?recent=true` by using live-shaped string telemetry fields.

## [0.1.0] - 2026-04-12

### Added
Expand All @@ -19,4 +46,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Rails integration with automatic client sharing and graceful shutdown.
- Interactive Playground app for manual testing and diagnostics.

[0.1.0]: https://github.com/octaspace/ruby-sdk/compare/v0.1.0...HEAD
[0.2.0]: https://github.com/octaspace/ruby-sdk/compare/v0.1.0...HEAD
[0.1.0]: https://github.com/octaspace/ruby-sdk/releases/tag/v0.1.0
35 changes: 28 additions & 7 deletions PUBLISHING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,29 @@

How to validate, build, and publish the `octaspace` gem.

Important release policy for this repository:

- there is no GitHub Actions workflow that publishes to RubyGems automatically
- publish only after the feature PR is reviewed, polished, and merged into `main`
- publish from a clean local checkout of `main`
- do not use `rake release` in this repository; build, push, tag, and release explicitly as separate manual steps

## 0. Release Sequence

1. Finish the feature branch work and merge the PR into `main`.
2. Switch to `main` locally and pull the merged commit.
3. Update `CHANGELOG.md` and bump `lib/octaspace/version.rb` if that was not already done in the merged branch.
4. Run verification locally.
5. Build the gem.
6. Push the gem to RubyGems manually.
7. Tag the exact published commit and push the tag.
8. Create the GitHub Release manually from that tag.

## 1. Pre-release Checklist

```bash
git switch main
git pull --ff-only
bundle exec rake test # all tests pass
bundle exec standardrb # zero linting violations
gem build octaspace.gemspec # builds without warnings
Expand All @@ -15,7 +35,7 @@ gem build octaspace.gemspec # builds without warnings
Install the built gem outside the project and confirm it loads correctly:

```bash
gem install ./octaspace-0.1.0.gem
gem install ./octaspace-0.2.0.gem
ruby -e "require 'octaspace'; p OctaSpace::Client.new"
```

Expand All @@ -35,30 +55,31 @@ Use these alternatives to verify the gem before publishing:

- **Inspect gem contents:**
```bash
gem unpack octaspace-0.1.0.gem --target=/tmp/octaspace-check
gem unpack octaspace-0.2.0.gem --target=/tmp/octaspace-check
find /tmp/octaspace-check -type f | sort
rm -rf /tmp/octaspace-check
```
- **Check metadata that RubyGems.org will display:**
```bash
gem specification octaspace-0.1.0.gem
gem specification octaspace-0.2.0.gem
```
- **Install and test in a clean environment:**
```bash
gem install ./octaspace-0.1.0.gem
gem install ./octaspace-0.2.0.gem
ruby -e "require 'octaspace'; p OctaSpace::Client.new; puts OctaSpace::VERSION"
```

## 4. Publish to RubyGems.org

```bash
gem push octaspace-0.1.0.gem
gem push octaspace-0.2.0.gem
```

You will be prompted for credentials on first push. The API key is saved to `~/.gem/credentials`.

## 5. Post-publish

- Verify the gem page at [rubygems.org/gems/octaspace](https://rubygems.org/gems/octaspace).
- Tag the release: `git tag v0.1.0 && git push --tags`.
- Bump `lib/octaspace/version.rb` for the next development cycle.
- Tag the exact published commit: `git tag v0.2.0 && git push origin v0.2.0`.
- Create the GitHub Release manually from tag `v0.2.0`.
- Bump `lib/octaspace/version.rb` for the next development cycle in a follow-up commit.
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,24 +62,28 @@ client.sessions.list # GET /sessions
session = client.services.session("uuid-here")
session.info # GET /services/:uuid/info
session.logs # GET /services/:uuid/logs
session.logs(recent: true) # GET /services/:uuid/logs?recent=true for finished sessions
session.stop(score: 5) # POST /services/:uuid/stop

# Services
client.services.mr.list # GET /services/mr
client.services.mr.list # GET /services/mr (marketplace machine catalog)
client.services.mr.create(
node_id: 1,
disk_size: 10,
image: "ubuntu:24.04",
app: "249b4cb3-3db1-4c06-98a4-772ba88cd81c"
) # POST /services/mr
client.services.vpn.list # GET /services/vpn
client.services.vpn.list # GET /services/vpn (VPN relay catalog)
client.services.vpn.create(node_id: 1, subkind: "wg") # POST /services/vpn
client.services.render.list # GET /services/render
client.services.render.create(node_id: 1, disk_size: 100) # POST /services/render

# Apps
client.apps.list # GET /apps

# Note: live API may serialize app port lists as JSON strings
# (for example "[]"). The raw response is preserved by the SDK.

# Network
client.network.info # GET /network

Expand Down Expand Up @@ -269,9 +273,9 @@ Pages:
|---|---|
| `/playground/account` | Profile + balance |
| `/playground/nodes` | Node list with state badges |
| `/playground/sessions` | Active sessions |
| `/playground/services` | Machine Rentals + VPN sessions |
| `/playground/diagnostics` | Transport mode, pool stats, URL rotator state (auto-refresh every 5s) |
| `/playground/sessions` | Current + recent sessions, including live-format quirks |
| `/playground/services` | Marketplace catalogs for MR, Render, and VPN |
| `/playground/diagnostics` | Direct SDK method runner for contracts, payloads, transport mode, and pool stats |

## Development

Expand All @@ -288,7 +292,7 @@ bundle exec rake test # tests only
You can verify the gem in a clean Ruby environment without Rails:

1. Build the gem: `gem build octaspace.gemspec`
2. Install it locally: `gem install ./octaspace-0.1.0.gem`
2. Install it locally: `gem install ./octaspace-0.2.0.gem`
3. Test in IRB:

```ruby
Expand All @@ -310,7 +314,7 @@ bundle exec appraisal rails-8-0 rake test

### Dummy Application (Playground)

The repository includes a Rails "Dummy" application for manual testing and UI prototyping. It is located in `test/dummy`.`.
The repository includes a Rails "Dummy" application for manual testing and UI prototyping. It is located in `test/dummy`.

To run the dummy app:

Expand Down
2 changes: 1 addition & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ if (requested_tasks & %w[rdoc clobber_rdoc]).any?
end
end

if (requested_tasks & %w[build install release]).any?
if (requested_tasks & %w[build install]).any?
require "bundler/gem_tasks"
end

Expand Down
1 change: 1 addition & 0 deletions lib/octaspace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require "octaspace/version"
require "octaspace/errors"
require "octaspace/payload_helpers"
require "octaspace/response"
require "octaspace/configuration"
require "octaspace/middleware/url_rotator"
Expand Down
26 changes: 26 additions & 0 deletions lib/octaspace/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,32 @@ class TimeoutError < NetworkError; end
# API-level errors (HTTP response received, but indicates failure)
class ApiError < Error; end

# Domain-level rejection where transport succeeded but the provision request
# was rejected by the API payload contract.
class ProvisionRejectedError < Error
attr_reader :rejections

def initialize(message = nil, response: nil, rejections: [])
@rejections = Array(rejections)
super(message || build_message(@rejections), response: response)
end

private

def build_message(rejections)
first_reason =
rejections.filter_map do |item|
next unless item.is_a?(Hash)

item["reason"] || item[:reason]
end.first

return "Provision request rejected" if first_reason.to_s.empty?

"Provision request rejected: #{first_reason}"
end
end

# 401 Unauthorized
class AuthenticationError < ApiError; end

Expand Down
53 changes: 53 additions & 0 deletions lib/octaspace/payload_helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# frozen_string_literal: true

require "json"

module OctaSpace
module PayloadHelpers
module_function

def parse_port_list(value)
case value
when nil
[]
when Array
value
when String
parse_stringified_port_list(value)
else
Array(value)
end
end

def normalize_marketplace_bandwidth(value)
numeric =
case value
when nil
return nil
when Numeric
value.to_f
when String
Float(value)
else
return value
end

return numeric unless numeric > 100_000

numeric / 125_000.0
rescue ArgumentError, TypeError
value
end

def parse_stringified_port_list(value)
stripped = value.strip
return [] if stripped.empty?

parsed = JSON.parse(stripped)
parsed.is_a?(Array) ? parsed : []
rescue JSON::ParserError
[]
end
private_class_method :parse_stringified_port_list
end
end
8 changes: 8 additions & 0 deletions lib/octaspace/playground/payload_presets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ presets:
uuid: sess-abc-123
score: 5

services.session.logs:
source:
live_api:
note: Uses the finished-session recent logs branch confirmed against the live API.
payload:
uuid: sess-abc-123
recent: true

idle_jobs.find:
source:
fixture: test/fixtures/idle_jobs/show.json
Expand Down
34 changes: 32 additions & 2 deletions lib/octaspace/resources/services/machine_rental.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class Services
# app: "249b4cb3-3db1-4c06-98a4-772ba88cd81c"
# )
class MachineRental < Base
# List available / active machine rentals
# List available marketplace machines for rent
# GET /services/mr
# @param params [Hash] optional filter params
# @return [OctaSpace::Response]
Expand All @@ -40,7 +40,37 @@ def create(**attrs)
entrypoint: attrs[:entrypoint].to_s
}

post("/services/mr", body: [item])
item[:organization_id] = attrs[:organization_id] if attrs.key?(:organization_id)
item[:project_id] = attrs[:project_id] if attrs.key?(:project_id)

response = post("/services/mr", body: [item])
raise_if_rejected!(response)
response
end

private

def raise_if_rejected!(response)
rejections = extract_rejections(response.data)
return if rejections.empty?

raise OctaSpace::ProvisionRejectedError.new(response: response, rejections: rejections)
end

def extract_rejections(data)
return [] unless data.is_a?(Array)

data.filter_map do |item|
next unless item.is_a?(Hash)

reason = item["reason"] || item[:reason]
status = item["status"] || item[:status]
uuid = item["uuid"] || item[:uuid]
next if uuid
next unless reason || status.to_i.positive?

item
end
end
end
end
Expand Down
9 changes: 7 additions & 2 deletions lib/octaspace/resources/services/session_proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,14 @@ def info

# Fetch session logs
# GET /services/:uuid/logs
# @param recent [Boolean, nil] use the recent logs branch for finished sessions
# @return [OctaSpace::Response]
def logs
@transport.get("/services/#{@uuid}/logs")
def logs(recent: nil)
params = {}
params[:recent] = true if recent
return @transport.get("/services/#{@uuid}/logs") if params.empty?

@transport.get("/services/#{@uuid}/logs", params:)
end

# Stop the session
Expand Down
2 changes: 1 addition & 1 deletion lib/octaspace/resources/services/vpn.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class Services
# client.services.vpn.list
# client.services.vpn.create(node_id: 123)
class Vpn < Base
# List active VPN sessions
# List available VPN relay nodes
# GET /services/vpn
# @param params [Hash] optional filter params
# @return [OctaSpace::Response]
Expand Down
2 changes: 1 addition & 1 deletion lib/octaspace/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module OctaSpace
VERSION = "0.1.0"
VERSION = "0.2.0"
end
Loading
Loading