Skip to content
Open
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
115 changes: 115 additions & 0 deletions .claude/skills/update-cloudhub-client/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
---
name: update-cloudhub-client
description: >
Re-sync this Java client (cloudhub-java-client) with the CloudHub .NET API: capture the
latest OpenAPI spec from CloudHub, regenerate the client, re-apply the protected
customizations, build, summarize what changed, and bump the version. Use whenever the
client must mirror new/changed CloudHub endpoints or models, after CloudHub is updated,
or when asked to "update / regenerate / sync the cloudhub java client".
---

# Update the Cloudhub Java client

This skill drives a mostly-deterministic pipeline and supplies the judgment the pipeline
can't. The mechanical work lives in `scripts/Update-JavaClient.ps1`, `openapi-generator.gradle`,
and `.openapi-generator-ignore`. Your job is to run it, read the spec diff, make the few
decisions a generator can't, and report clearly.

## How the pieces fit (read first)

- The client mirrors CloudHub's **OpenAPI spec** — it has no binary dependency on CloudHub.
- `openapi/cloudhub.json` is the committed **source of truth**. `git diff` on it = "what changed upstream".
- Code generation regenerates everything under `src/main/java/**`, `src/test/java/**`, `docs/**`
**except** files in `.openapi-generator-ignore` (the hand-written `CloudhubClient.java`,
`CloudhubUtils.java`, `build.gradle`, `README.md`, `CHANGELOG.md`, gradle wrapper, this skill).
- `hideGenerationTimestamp=true` keeps generator output stable, so a re-run with no upstream
change produces an **empty diff**.

## Prerequisites

- **.NET SDK** compatible with CloudHub's target framework (currently `net10.0`), and access to
restore CloudHub's NuGet dependencies (private Lacuna feed or a warm package cache).
- **JDK 8+** on `PATH` (or `JAVA_HOME` set) — Gradle and the generator need it.
- The **CloudHub repo** checked out, by default as a sibling folder `../cloudhub`.

## Procedure

### 1. Capture the new spec
Run capture-only first so you can inspect the diff before regenerating:
```
pwsh scripts/Update-JavaClient.ps1 -SpecOnly
```
This builds CloudHub, then writes `openapi/cloudhub.json` via the Swashbuckle CLI tool
(`dotnet swagger tofile ... v1`). If the build/capture fails, see **Troubleshooting**.

### 2. Review what changed upstream
```
git diff -- openapi/cloudhub.json
```
Read the diff and write a short human summary grouped as:
- **Added** paths / schemas / properties
- **Removed** paths / schemas / properties ← breaking
- **Changed** types, `required`, enum members, `format` ← often breaking

### 3. Classify the change → decide the version bump
Use [semver](https://semver.org) against the current `version` in `build.gradle`:

| Change | Bump |
|---|---|
| Only additive (new endpoints/models/optional fields) | **minor** |
| Removed/renamed endpoint, field, or enum member; a property became `required`; a type or wire `format` changed; an enum's underlying type changed (e.g. integer→string) | **major** |
| Doc-only / cosmetic | **patch** |

State the recommended new version and the reason. CloudHub's own version is a useful signal
but the client's Maven version is independent — bump it on its own merits.

### 4. Handle judgment items the generator can't (FLAG, don't guess)
Scan the spec diff for these and resolve each explicitly:

- **New INTEGER-valued enum** (`{"enum": [1,2,...], "type": "integer"}`): the generator would emit
`NUMBER_1`, `NUMBER_2`, …. Add a mapping to `openapi/overrides.json` under `enumVarnames`
(schema name → ordered constant names). String enums self-name — no action.
- **New endpoint returning `string/format: byte` or a stream**: the existing client provides
helpers in `cloudhub/CloudhubUtils.java` (`convertCertificateToString`,
`convertToSignHashToByteArray64`). Consider whether the new endpoint warrants a similar
convenience helper; if unsure, **flag it for a human** rather than inventing one.
- **Removed endpoints/fields**: confirm intentional, call it out as breaking in the summary.
- **New auth scheme**: the generated `ApiClient` wires schemes from the spec automatically, but
`CloudhubClient`'s convenience constructor only knows `X-Api-Key`. Update `CloudhubClient.java`
if a new scheme must be first-class.

### 5. Regenerate + build
```
pwsh scripts/Update-JavaClient.ps1 -SkipCloudHubBuild
```
(`-SkipCloudHubBuild` reuses the assemblies from step 1.) This runs `openApiGenerate`, the
idempotent JavaDoc safety patch, then `clean build`. The build must be green.

### 6. Finalize
- Bump `version` in `build.gradle` to the value from step 3 (the generator reads it for `artifactVersion`).
- Prepend an entry to `CHANGELOG.md`: new version, date, and the step-2 summary.
- Re-run `git status` / `git diff` and sanity-check that **only** intended files changed and that
the protected files (`CloudhubClient.java`, `CloudhubUtils.java`) are untouched.

### 7. Report
Tell the user: the new version, the upstream-change summary, any flagged judgment items and how
you resolved them, and the build result. Do **not** commit/push unless asked.

## Verifying the result
- `./gradlew clean build` is green.
- Idempotence: a second `-SkipCloudHubBuild` run with no upstream change leaves an empty `git diff`.
- Protected customizations intact: `CloudhubClient extends ApiClient` with the `(baseURL, apiKey)`
ctor + 2-minute timeouts; `CloudhubUtils` helpers present.

## Troubleshooting
- **CloudHub build fails on restore**: the private Lacuna NuGet feed isn't configured. Add it
(`dotnet nuget add source ...`) or build on a machine with a warm package cache. If you only
have a pre-exported `swagger.json`, copy it to `openapi/cloudhub.json` and skip to step 2.
- **`dotnet swagger tofile` fails**: confirm the Swagger doc name is still `v1`
(`Site/Startup.cs` → `SwaggerDoc("v1", ...)`); pass the matching name as the last arg.
- **Generation overwrote a hand-written file**: it isn't listed in `.openapi-generator-ignore` —
add it and regenerate.
- **JavaDoc build error about `<table summary>`**: the safety patch in the script handles it; if
it recurs, add a `templates/api_doc.mustache` override (the gradle config auto-uses `templates/`).
- **Spurious diffs on every run**: ensure `hideGenerationTimestamp=true` is still set in
`openapi-generator.gradle`.
13 changes: 13 additions & 0 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"swashbuckle.aspnetcore.cli": {
"version": "10.1.7",
"commands": [
"swagger"
],
"rollForward": false
}
}
}
13 changes: 13 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Normalize line endings so code generation produces content-only diffs whether it runs
# locally on Windows or in Linux CI. Generated sources use LF; without this, CI (LF) vs a
# Windows-committed baseline (CRLF) shows every line as changed in the PR.
* text=auto eol=lf

# Windows batch scripts keep CRLF
*.bat text eol=crlf

# Binary assets (never touch line endings)
*.jar binary
*.gpg binary
*.p12 binary
*.pfx binary
90 changes: 90 additions & 0 deletions .github/workflows/sync-from-cloudhub.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
name: Sync from CloudHub (regenerate client)

# Regenerates this client from CloudHub's current OpenAPI spec and opens a PR.
# Triggers:
# - repository_dispatch : fired by the cloudhub repo's "Trigger Java client sync" workflow
# - schedule : weekly poll (reads CloudHub's CHANGELOG as the change signal)
# - workflow_dispatch : manual
#
# ALL secrets below are PLACEHOLDERS — fill them in later.

on:
repository_dispatch:
types: [regenerate-client]
schedule:
- cron: '0 6 * * 1'
workflow_dispatch: {}

permissions:
contents: write
pull-requests: write

jobs:
regenerate:
# A hosted VM. If building CloudHub needs Windows/licensed components or a private
# network, switch to a self-hosted runner: runs-on: [self-hosted, windows]
runs-on: ubuntu-latest

steps:
- name: Checkout java client
uses: actions/checkout@v4

- name: Checkout CloudHub (.NET server) to build + capture the spec
uses: actions/checkout@v4
with:
repository: LacunaSoftware/cloudhub
path: cloudhub
token: ${{ secrets.CLOUDHUB_REPO_TOKEN }} # PLACEHOLDER: read access to the private cloudhub repo

- name: Read CloudHub CHANGELOG (the trigger signal / target version)
run: |
echo "----- CloudHub CHANGELOG (top) -----"
head -n 40 cloudhub/CHANGELOG.md || echo "(no CHANGELOG.md in cloudhub)"

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'

- name: Configure private Lacuna NuGet feed
run: |
dotnet nuget add source "${{ secrets.LACUNA_NUGET_FEED_URL }}" \
--name lacuna \
--username "${{ secrets.LACUNA_NUGET_USER }}" \
--password "${{ secrets.LACUNA_NUGET_TOKEN }}" \
--store-password-in-clear-text
# PLACEHOLDERS: creds for restoring Lacuna.Pki / BrazilTrustServices packages

- name: Build CloudHub + capture OpenAPI spec (no tests)
run: |
dotnet build cloudhub/Site/Lacuna.Cloudhub.Site.csproj -c Release --nologo
dotnet tool restore
DLL="$(find cloudhub/Site/bin/Release -name Lacuna.Cloudhub.Site.dll | head -n1)"
dotnet swagger tofile --output openapi/cloudhub.json "$DLL" v1

- name: Setup JDK
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'

- name: Code generation only (no build/tests)
run: ./gradlew normalizeOpenApiSpec openApiGenerate --console=plain

- name: Claude Code — finalize (version + changelog) and open PR
uses: anthropics/claude-code-action@v1
with:
# ┌─ PLACEHOLDER: choose ONE Claude auth method ───────────────────────────────┐
# │ (a) Teams/Enterprise seat via OAuth token (claude setup-token): │
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# │ (b) or a Console service-account API key: │
# anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} │
# └────────────────────────────────────────────────────────────────────────────┘
prompt: >
Follow the /update-cloudhub-client skill. The CloudHub spec has already been captured
to openapi/cloudhub.json and the client regenerated (./gradlew openApiGenerate ran).
Do NOT run app tests. Review the generated diff against CloudHub's changelog at
./cloudhub/CHANGELOG.md, bump the version in build.gradle accordingly, update this
repo's CHANGELOG.md, then open a pull request summarizing the API changes. If there is
no effective change, do not open a PR.
claude_args: "--model opus"
23 changes: 12 additions & 11 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,33 @@
# Mobile Tools for Java (J2ME)
.mtj.tmp/

# Package Files #
# Package files
*.jar
*.war
*.ear

# exclude jar for gradle wrapper
!gradle/wrapper/*.jar

# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
# JVM crash logs
hs_err_pid*

# build files
# Build output
**/target
target
.gradle
build
.github/workflows/maven.yml
git_push.sh

# Local / machine-specific / secrets
gradle.properties
.openapi-generator/*

# OpenAPI Generator byproducts we don't use (also suppressed in .openapi-generator-ignore
# so they are never generated; ignored here as a safety net).
.openapi-generator/
pom.xml
.openapi-generator/*
api/openapi.yaml
build.sbt
.travis.yml
git_push.sh
api/openapi.yaml
src/main/AndroidManifest.xml
.openapi-generator-ignore
.github/workflows/maven.yml
*.gpg
Comment thread
EduardoFMC-lacuna marked this conversation as resolved.
.vscode/settings.json
59 changes: 59 additions & 0 deletions .openapi-generator-ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# OpenAPI Generator ignore file (committed; honored via `ignoreFileOverride` in openapi-generator.gradle).
#
# Files matched here are NEVER overwritten by `openApiGenerate`. Everything else under
# src/main/java/**, src/test/java/**, and docs/** IS regenerated on every run.
# Pattern syntax is .gitignore-style, rooted at the repository root (the generator outputDir).

############################
# Hand-written source code #
############################
# Convenience entry point: extends the generated ApiClient with a (baseURL, apiKey)
# constructor and default 2-minute timeouts. See openapi-generator.gradle / README.
src/main/java/cloudhub/client/CloudhubClient.java
# Lacuna-specific helpers, moved out of the (regenerated) SessionsApi so they survive.
src/main/java/cloudhub/CloudhubUtils.java

###########################
# Build & publish config #
###########################
build.gradle
settings.gradle
gradle.properties
gradlew
gradlew.bat
gradle/**
openapi-generator.gradle

###########################
# Pipeline inputs/tooling #
###########################
.openapi-generator-ignore
openapi/**
templates/**
scripts/**
.config/**
.github/**
.claude/**

##################
# Docs & project #
##################
README.md
CHANGELOG.md
LICENSE

##########################################
# Generator byproducts we don't use #
# (this is a Gradle project - not Maven/ #
# SBT/Travis/Android; spec lives in #
# openapi/cloudhub.json) #
##########################################
pom.xml
build.sbt
.travis.yml
git_push.sh
api/**
src/main/AndroidManifest.xml

# Never overwrite the hand-maintained VCS ignore file (the generator emits its own .gitignore)
.gitignore
47 changes: 47 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Changelog

All notable changes to the Cloudhub Java client are documented here.
This project adheres to [Semantic Versioning](https://semver.org).

## [2.0.0] - 2026-06-30

### Fixed
- **Client sends the `X-Api-Key` header.** CloudHub's spec declares the `ApiKey` security *scheme*
but no *requirement* (its `AddSecurityRequirement` is commented out), so the generated operations
had empty `authNames` and every authenticated call failed with **HTTP 401**. The
`normalizeOpenApiSpec` step injects a global security requirement
(`openapi/overrides.json` → `globalSecurity: [{ ApiKey: [] }]`) so all operations attach the key.

### Added — automated CloudHub sync pipeline
- `openapi/cloudhub.json` — OpenAPI spec captured from CloudHub, committed as the source of truth.
- `openapi-generator.gradle` — pinned OpenAPI Generator (7.14.0) config and a `normalizeOpenApiSpec`
task (the declarative spec-fix hook, driven by `openapi/overrides.json`).
- `.openapi-generator-ignore` — protects hand-maintained files from regeneration (now committed).
- `scripts/Update-JavaClient.ps1` — one command: capture spec → regenerate → build.
- `.config/dotnet-tools.json` — pins the Swashbuckle CLI used for headless spec capture.
- `.claude/skills/update-cloudhub-client` — skill that orchestrates the pipeline, summarizes the
upstream diff, recommends the version bump, and flags judgment calls.
- `cloudhub.CloudhubUtils` — new home for the `convertCertificateToString` /
`convertToSignHashToByteArray64` helpers (previously inside the generated `SessionsApi`).

### Changed
- `cloudhub.client.CloudhubClient` is now a thin subclass of the generated `cloudhub.client.ApiClient`
(keeping the `(baseURL, apiKey)` constructor and 2-minute timeouts) instead of a renamed copy of the
generated invoker. Existing `new CloudhubClient(endpoint, apiKey)` usage is unchanged. (`setBasePath`
now returns `ApiClient` rather than `CloudhubClient`, affecting only fluent chaining.)
- `build.gradle`: signing is now required only when publishing to the **remote** Maven repository, and
the OSSRH credentials resolve null-safely. This lets regeneration, `build`, and `publishToMavenLocal`
run without a GPG key or credentials (CI / local testing); the real `publish` to Sonatype is unchanged.

### Migration note (source compatibility)
- The two helpers moved from `cloudhub.SessionsApi` to `cloudhub.CloudhubUtils`. Update any callers:
`SessionsApi.convertCertificateToString(...)` → `CloudhubUtils.convertCertificateToString(...)`.

### Updated to the CloudHub 2.0.0 API surface
Regenerated from the current CloudHub spec. Changes since client 1.0.5:
- **Added** endpoints: `GET /api/sessions/services/{name}/availability`, `GET /api/sessions/custom-state`.
- **Added** model `GetServiceAvailabilityResponse`; added `CertificateModel.serviceName`.
- **Changed** `SessionCreateRequest`: `lifetimeInMinutes` → `lifetimeInSeconds` (int32); added
`identifierType`; `redirectUri` is now required.
- **Changed (breaking)** `TrustServiceSessionTypes` is now a **string** enum (was integer) — the wire
format changed upstream. This alone implies a **major** version bump.
Loading