From b6758b7fc2adeb8dea9ddfc3ac36050dffb1dec1 Mon Sep 17 00:00:00 2001 From: Jacob Page Date: Tue, 1 Sep 2026 10:14:54 -0700 Subject: [PATCH 1/2] fix(api-catalog): resync generated specs with upstream drift Domains v3 gained a getRegistrationSchema endpoint (and RegistrationSchema type), and the hosting-nodejs v1 spec added deprecation notices on rollbackApp and updateAppSecrets. Regenerated via `cargo run -p generate-api-catalog`. Co-Authored-By: Claude Sonnet 5 --- rust/domains-client/openapi/domains.oas3.json | 85 +++++++++++++++++++ rust/schemas/api/domains.json | 84 ++++++++++++++++++ rust/schemas/api/hosting-nodejs.json | 2 + rust/schemas/api/manifest.json | 4 +- .../openapi/hosting-nodejs-public-v1.yaml | 9 ++ 5 files changed, 182 insertions(+), 2 deletions(-) diff --git a/rust/domains-client/openapi/domains.oas3.json b/rust/domains-client/openapi/domains.oas3.json index 76ecb58e..f2e2cc70 100644 --- a/rust/domains-client/openapi/domains.oas3.json +++ b/rust/domains-client/openapi/domains.oas3.json @@ -140,6 +140,16 @@ "$ref": "#/components/schemas/uuid" } }, + "tld": { + "description": "The Top-Level Domain (e.g. com, net, io) whose registration schema should be retrieved.\n", + "example": "com", + "in": "path", + "name": "tld", + "required": true, + "schema": { + "type": "string" + } + }, "totalRequired": { "description": "When true, the response includes totalItems and totalPages for the current filter when at least one record matches. Both are omitted when the result set is empty. Defaults to false; omitting totals avoids the cost of a count query on large collections.\n", "example": true, @@ -1580,6 +1590,13 @@ "title": "Registration Quote", "type": "object" }, + "RegistrationSchema": { + "$id": "https://godaddy.com/schemas/domains/domain-lifecycle/registration-schema.v3", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "TLD-specific JSON Schema describing the fields required to register a domain under this TLD. The structure varies per TLD and is not further constrained at the API layer. Use the contents to guide construction of the POST /registration-quotes and POST /registrations request bodies.\n", + "title": "Registration Schema", + "type": "object" + }, "ResolvedSettings": { "$id": "https://godaddy.com/schemas/domains/domain-lifecycle/resolved-settings.v3", "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -3302,6 +3319,74 @@ ] } }, + "/v3/domains/registration-schemas/{tld}": { + "get": { + "description": "Returns the TLD-specific JSON Schema describing the fields required\nwhen calling POST /registration-quotes or POST /registrations for\nthe given TLD. The schema shape varies by TLD; clients should call\nthis endpoint before quoting or registering to discover required\ncontact and eligibility fields.\n", + "operationId": "getRegistrationSchema", + "parameters": [ + { + "description": "Optional client-generated request correlation identifier, propagated across services and returned in the response X-Request-Id header.\n", + "in": "header", + "name": "X-Request-Id", + "required": false, + "schema": { + "$ref": "#/components/schemas/uuid" + } + }, + { + "description": "Reseller acting on behalf of a shopper account. When present, all domain operations are scoped to the specified shopper. Absent, the authenticated entity's own account is used. Only valid for reseller OAuth tokens.\n", + "example": "shopper_123", + "in": "header", + "name": "X-Shopper-Id", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "The Top-Level Domain (e.g. com, net, io) whose registration schema should be retrieved.\n", + "example": "com", + "in": "path", + "name": "tld", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegistrationSchema" + } + } + }, + "description": "TLD-specific registration schema.", + "headers": { + "X-Request-Id": { + "description": "Request correlation identifier echoed from the request or server-generated.", + "schema": { + "$ref": "#/components/schemas/uuid" + } + } + } + } + }, + "security": [ + { + "oauth2": [ + "domains.domain:read" + ] + } + ], + "summary": "Retrieve the registration schema for a TLD", + "tags": [ + "Registrations" + ] + } + }, "/v3/domains/registrations": { "post": { "description": "Executes a previously quoted domain registration. **Irreversible once\naccepted; creates a charge.** Requires a valid unexpired quoteToken from\n`quoteDomainRegistration`, an `Idempotency-Key` header, and a consent\nrecord. The target domain and period are in the request body alongside\nthe quoteToken.\n\nIdempotency takes precedence over the single-use check: retrying with\nthe same `Idempotency-Key` replays the original operation even after\nthe token is consumed.\n\nReturns a `Registration` entity. Poll `links[rel=self]`\n(`GET /registrations/{registrationId}`) until status is `COMPLETED` or\n`FAILED`. The `operationId` field is also provided for clients that\nprefer `GET /operations/{operationId}`; both resolve the same resource.\n\nPoll either until status is `COMPLETED` or `FAILED`. The operation is\nfire-and-forget; always poll at least once even if the server completed\nit synchronously.\n\nWhen `iscCode` was supplied at quote time, the same value must be\nprovided here or the request fails with `422 quote_mismatch`.\n\n**PREMIUM domains:** when the quote's `fees` array is non-empty\n(inventory `PREMIUM`), the execute request must include\n`consent.acknowledgedFees` containing the same fees verbatim —\nsame types, amounts, and currencies. This confirms the customer\nexplicitly saw and accepted the specific charge before the\nirreversible purchase is executed.\n\n- `acknowledgedFees` absent when fees exist → `422` with error\n name `consent_fees_required` (conditionally required by the quote; the\n constraint spans two requests and cannot be expressed in the\n schema).\n- `acknowledgedFees` present but type or amount does not match\n the locked quote → `422` with error name `quote_mismatch`.\n", diff --git a/rust/schemas/api/domains.json b/rust/schemas/api/domains.json index 7841faba..56a9d94f 100644 --- a/rust/schemas/api/domains.json +++ b/rust/schemas/api/domains.json @@ -1284,6 +1284,13 @@ "title": "Registration Quote", "type": "object" }, + "RegistrationSchema": { + "$id": "https://godaddy.com/schemas/domains/domain-lifecycle/registration-schema.v3", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "TLD-specific JSON Schema describing the fields required to register a domain under this TLD. The structure varies per TLD and is not further constrained at the API layer. Use the contents to guide construction of the POST /registration-quotes and POST /registrations request bodies.\n", + "title": "Registration Schema", + "type": "object" + }, "ResolvedSettings": { "$id": "https://godaddy.com/schemas/domains/domain-lifecycle/resolved-settings.v3", "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -2671,6 +2678,83 @@ ], "summary": "Quote a single-domain registration (no commitment)" }, + { + "description": "Returns the TLD-specific JSON Schema describing the fields required\nwhen calling POST /registration-quotes or POST /registrations for\nthe given TLD. The schema shape varies by TLD; clients should call\nthis endpoint before quoting or registering to discover required\ncontact and eligibility fields.\n", + "method": "GET", + "operationId": "getRegistrationSchema", + "parameters": [ + { + "description": "Optional client-generated request correlation identifier, propagated across services and returned in the response X-Request-Id header.\n", + "in": "header", + "name": "X-Request-Id", + "required": false, + "schema": { + "$ref": "#/$defs/uuid" + } + }, + { + "description": "Reseller acting on behalf of a shopper account. When present, all domain operations are scoped to the specified shopper. Absent, the authenticated entity's own account is used. Only valid for reseller OAuth tokens.\n", + "in": "header", + "name": "X-Shopper-Id", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "The Top-Level Domain (e.g. com, net, io) whose registration schema should be retrieved.\n", + "in": "path", + "name": "tld", + "required": true, + "schema": { + "type": "string" + } + } + ], + "path": "/registration-schemas/{tld}", + "responses": { + "200": { + "description": "TLD-specific registration schema.", + "schema": { + "$ref": "#/$defs/RegistrationSchema" + } + }, + "400": { + "description": "Malformed request syntax, missing required field, or invalid field type.", + "schema": { + "$ref": "#/$defs/error" + } + }, + "401": { + "description": "Authentication credentials are missing or invalid.", + "schema": { + "$ref": "#/$defs/error" + } + }, + "403": { + "description": "Authenticated identity is not authorized to perform this operation.", + "schema": { + "$ref": "#/$defs/error" + } + }, + "404": { + "description": "The requested resource was not found.", + "schema": { + "$ref": "#/$defs/error" + } + }, + "429": { + "description": "Too many requests — rate limit exceeded.", + "schema": { + "$ref": "#/$defs/error" + } + } + }, + "scopes": [ + "domains.domain:read" + ], + "summary": "Retrieve the registration schema for a TLD" + }, { "description": "Executes a previously quoted domain registration. **Irreversible once\naccepted; creates a charge.** Requires a valid unexpired quoteToken from\n`quoteDomainRegistration`, an `Idempotency-Key` header, and a consent\nrecord. The target domain and period are in the request body alongside\nthe quoteToken.\n\nIdempotency takes precedence over the single-use check: retrying with\nthe same `Idempotency-Key` replays the original operation even after\nthe token is consumed.\n\nReturns a `Registration` entity. Poll `links[rel=self]`\n(`GET /registrations/{registrationId}`) until status is `COMPLETED` or\n`FAILED`. The `operationId` field is also provided for clients that\nprefer `GET /operations/{operationId}`; both resolve the same resource.\n\nPoll either until status is `COMPLETED` or `FAILED`. The operation is\nfire-and-forget; always poll at least once even if the server completed\nit synchronously.\n\nWhen `iscCode` was supplied at quote time, the same value must be\nprovided here or the request fails with `422 quote_mismatch`.\n\n**PREMIUM domains:** when the quote's `fees` array is non-empty\n(inventory `PREMIUM`), the execute request must include\n`consent.acknowledgedFees` containing the same fees verbatim —\nsame types, amounts, and currencies. This confirms the customer\nexplicitly saw and accepted the specific charge before the\nirreversible purchase is executed.\n\n- `acknowledgedFees` absent when fees exist → `422` with error\n name `consent_fees_required` (conditionally required by the quote; the\n constraint spans two requests and cannot be expressed in the\n schema).\n- `acknowledgedFees` present but type or amount does not match\n the locked quote → `422` with error name `quote_mismatch`.\n", "method": "POST", diff --git a/rust/schemas/api/hosting-nodejs.json b/rust/schemas/api/hosting-nodejs.json index 4621afc9..878e72fc 100644 --- a/rust/schemas/api/hosting-nodejs.json +++ b/rust/schemas/api/hosting-nodejs.json @@ -1697,6 +1697,7 @@ "summary": "Get application logs" }, { + "description": "**Deprecated.** This operation is not available in v2 per API design review. It remains live and feature-flagged in production but may be removed in a future v1 maintenance release. Clients should not build new integrations against this endpoint.", "method": "POST", "operationId": "rollbackApp", "parameters": [ @@ -1812,6 +1813,7 @@ "summary": "List app secrets (metadata only)" }, { + "description": "Superseded by `POST /v2/hosting/nodejs/apps/{appId}/sync-secrets`, which accepts the same operation shape under the `hosting.secret:write` OAuth scope and returns the same secret-metadata response. This v1 endpoint remains live.", "method": "POST", "operationId": "updateAppSecrets", "parameters": [ diff --git a/rust/schemas/api/manifest.json b/rust/schemas/api/manifest.json index b493382a..717b7683 100644 --- a/rust/schemas/api/manifest.json +++ b/rust/schemas/api/manifest.json @@ -1,5 +1,5 @@ { - "generated": "2026-08-26T18:46:42.423872066+00:00", + "generated": "2026-09-01T16:02:33.125619062+00:00", "domains": { "bulk-operations": { "file": "bulk-operations.json", @@ -34,7 +34,7 @@ "domains": { "file": "domains.json", "title": "Domain Lifecycle Management API", - "endpointCount": 16 + "endpointCount": 17 }, "fulfillments": { "file": "fulfillments.json", diff --git a/rust/schemas/openapi/hosting-nodejs-public-v1.yaml b/rust/schemas/openapi/hosting-nodejs-public-v1.yaml index f191429c..4b916d2f 100644 --- a/rust/schemas/openapi/hosting-nodejs-public-v1.yaml +++ b/rust/schemas/openapi/hosting-nodejs-public-v1.yaml @@ -667,6 +667,10 @@ paths: post: operationId: updateAppSecrets summary: Add, update, or delete app secrets + description: Superseded by `POST /v2/hosting/nodejs/apps/{appId}/sync-secrets`, + which accepts the same operation shape under the `hosting.secret:write` + OAuth scope and returns the same secret-metadata response. This v1 + endpoint remains live. tags: - Secrets security: @@ -758,6 +762,11 @@ paths: post: operationId: rollbackApp summary: Rollback to a previous deployment + deprecated: true + description: "**Deprecated.** This operation is not available in v2 per API + design review. It remains live and feature-flagged in production but may + be removed in a future v1 maintenance release. Clients should not build + new integrations against this endpoint." tags: - Deployments security: From 129dc32d0fe398e9e48801b201cd96ed968064da Mon Sep 17 00:00:00 2001 From: Jacob Page Date: Tue, 1 Sep 2026 10:15:17 -0700 Subject: [PATCH 2/2] fix(ci): test the whole cargo workspace, not just the root package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `cargo test`/`check`/`clippy` with no flags only cover the root package in a workspace that also declares one (this one does: godaddy-cli plus domains-client and generate-api-catalog as members, no default-members override). CICD and the main-alpha workflow both ran plain `cargo test`, so domains-client's test module has been silently skipped since the workspace was split into multiple crates — its tests never compiled, on PRs or otherwise. That hid five compile errors and one runtime bug in domains-client's own tests, all pre-existing and unrelated to this branch's spec resync (confirmed against unmodified main): `optimize_for` became optional, `ConsentActorType`/`AgreementType` moved from generated enums to open string newtypes, `DnsRecord` gained a required `parameters` field, `Agreements`'s `privacy` setter was renamed to `v1_privacy`, and the `v1-agreements` mock asserted the wrong wire query-parameter name (`privacy` instead of the API's actual `v1-privacy`) once the compile errors above stopped masking it. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/cicd.yml | 6 +++--- .github/workflows/rust-port-alpha.yaml | 4 ++-- AGENTS.md | 14 ++++++++------ rust/domains-client/src/lib.rs | 11 ++++++----- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index a5127fc4..610b6e64 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -38,16 +38,16 @@ jobs: restore-keys: ${{ runner.os }}-cargo- - name: Check - run: cargo check + run: cargo check --workspace - name: Format run: cargo fmt --check - name: Lint - run: cargo clippy --all-targets -- -D warnings + run: cargo clippy --workspace --all-targets -- -D warnings - name: Test - run: cargo test + run: cargo test --workspace - name: Smoke test run: ./scripts/smoke-test.sh diff --git a/.github/workflows/rust-port-alpha.yaml b/.github/workflows/rust-port-alpha.yaml index 6f45e940..1ebcb221 100644 --- a/.github/workflows/rust-port-alpha.yaml +++ b/.github/workflows/rust-port-alpha.yaml @@ -54,10 +54,10 @@ jobs: restore-keys: ${{ runner.os }}-cargo-test- - name: Clippy - run: cargo clippy --all-targets -- -D warnings + run: cargo clippy --workspace --all-targets -- -D warnings - name: Test - run: cargo test + run: cargo test --workspace build: name: Build ${{ matrix.target }} diff --git a/AGENTS.md b/AGENTS.md index ff24e1ef..06444ba9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,10 +6,12 @@ This is a command-line application. Source code is written in Rust and lives und - **Build**: `cargo build` - **Run**: `cargo run -- ` -- **Test**: `cargo test` -- **Lint**: `cargo clippy -- -D warnings` +- **Test**: `cargo test --workspace` (the workspace has a root package, so plain + `cargo test` silently skips the `domains-client` and `generate-api-catalog` + members — always pass `--workspace`) +- **Lint**: `cargo clippy --workspace -- -D warnings` - **Format**: `cargo fmt` -- **Check**: `cargo check` +- **Check**: `cargo check --workspace` - **Refresh API specs**: `cargo run -p generate-api-catalog` — set `SKIP_DOMAINS_REFRESH`/`SKIP_HOSTING_REFRESH` to skip either pull for local iteration without network access; `HOSTING_SPEC_URL`/`HOSTING_SPEC_PATH` @@ -17,9 +19,9 @@ This is a command-line application. Source code is written in Rust and lives und ## Verification Checklist (required before finishing work) -- `cargo check` — must pass -- `cargo clippy -- -D warnings` — must pass with zero warnings -- `cargo test` — must pass +- `cargo check --workspace` — must pass +- `cargo clippy --workspace -- -D warnings` — must pass with zero warnings +- `cargo test --workspace` — must pass - `cargo fmt --check` — must be clean - `./rust/scripts/check-module-size.sh` — must pass diff --git a/rust/domains-client/src/lib.rs b/rust/domains-client/src/lib.rs index 5b2433a6..76379b4e 100644 --- a/rust/domains-client/src/lib.rs +++ b/rust/domains-client/src/lib.rs @@ -248,7 +248,7 @@ mod tests { .check_availability() .body(types::AvailabilityCheckCriteria { domains: vec!["a.com".to_string(), "b.com".to_string()], - optimize_for: types::OptimizationTarget::Speed, + optimize_for: Some(types::OptimizationTarget::Speed), isc_code: None, }) .send() @@ -361,9 +361,9 @@ mod tests { actor: None, ip: Some("127.0.0.1".to_string()), principal: "shopper-42".to_string(), - type_: types::ConsentActorType::Direct, + type_: types::ConsentActorType("DIRECT".to_string()), }), - agreement_types: vec![types::AgreementType::ApiDpa], + agreement_types: vec![types::AgreementType("API_DPA".to_string())], acknowledged_fees: vec![], }, created_at: None, @@ -474,6 +474,7 @@ mod tests { data: "1.2.3.4".to_string(), flag: None, name: "www".to_string(), + parameters: None, port: None, priority: None, protocol: None, @@ -540,7 +541,7 @@ mod tests { when.method(GET) .path("/v1/domains/agreements") .query_param("tlds", "com") - .query_param("privacy", "false"); + .query_param("v1-privacy", "false"); then.status(200).json_body(json!([ { "agreementKey": "DNRA", "title": "Registration Agreement", "url": "https://x" } ])); @@ -550,7 +551,7 @@ mod tests { let agreements = client_for(&server) .agreements() .tlds(vec!["com".to_string()]) - .privacy(false) + .v1_privacy(false) .send() .await .expect("request succeeds")