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
49 changes: 41 additions & 8 deletions .agents/skills/release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ allowed-tools: >
Bash(gh pr merge * --repo G-Core/gcore-python *),
Bash(gh release view --repo G-Core/gcore-python *),
Bash(gh release edit * --repo G-Core/gcore-python *),
Bash(gh release view * --repo G-Core/gcore-go *),
Bash(gh release list --repo G-Core/gcore-go *),
Bash(sleep *)
---

Expand All @@ -23,7 +25,8 @@ allowed-tools: >
## Constraints

- **Repository**: `G-Core/gcore-python` (hardcoded, do not use for other repos)
- **Allowed tools**: `gh` CLI (scoped to `G-Core/gcore-python`) and `Read`
- **Allowed tools**: `gh` CLI (scoped to `G-Core/gcore-python` + read-only
`G-Core/gcore-go` releases) and `Read`
- **Never**: modify source code, force-push, delete branches, or merge without
explicit user confirmation
- **Release PRs** are created by `stainless-app[bot]` with title `release: {version}`
Expand Down Expand Up @@ -98,19 +101,41 @@ Fetch all three in parallel:
Within a product area, split into distinct **sub-areas** by resource type.
Do not lump unrelated resources into a single sub-area.

### Step 2.5 — Check Go SDK Release (Cross-SDK Sync)

Check whether `G-Core/gcore-go` already has a release for the same
version. Both SDKs are generated from the same API specs and share version
numbers, so matching releases cover the same underlying API changes.

```bash
gh release view v{VERSION} --repo G-Core/gcore-go --json tagName,body
```

- If a matching release **exists**, extract its Part 1 (everything before the
`## {VERSION}` auto-generated changelog heading). Store it as the **Go
reference notes** for use in Step 4.
- If the release **does not exist** (exit code ≠ 0), proceed without reference.
This is expected when the Python SDK releases first.

Do **not** display the Go notes to the user — they are an internal
reference for wording alignment only.

### Step 3 — Check CI Status

```bash
gh pr checks {N} --repo G-Core/gcore-python --json name,state,bucket
```

Exit codes: `0` = all pass, `8` = pending, `1` = failure.
**Ignore `detect-breaking-changes`** when evaluating CI status — it is
informational only. Breaking API changes are expected in release PRs and
documented in the changelog. If it fails, note it for the user but do not
treat it as a blocker.

| Status | Action |
|---|---|
| exit `0` / all checks pass | Report **CI green**, proceed |
| exit `8` / pending | Warn user checks are running. Ask: wait or proceed? |
| exit `1` / failure | Show failing checks. **Do not offer to merge.** |
After excluding `detect-breaking-changes`:

- **All checks pass** — report CI green, proceed.
- **Some checks pending** — warn user, ask: wait or proceed?
- **Any check fails** — show failing checks, do not offer to merge.

### Step 4 — Generate Human-Readable Release Notes

Expand Down Expand Up @@ -149,6 +174,12 @@ We're excited to announce version {VERSION}!
- **Always use backtick-wrapped Python identifiers** for types, fields, methods.
Types use `PascalCase`, methods/fields use `snake_case`.
- **No commit hashes or links** in Part 1 (those are in Part 2).
- **Cross-SDK alignment**: If Go reference notes were fetched in Step 2.5,
use them as a wording guide for overlapping API changes. For each change that
appears in both SDKs, match the Go description's phrasing and structure
while substituting Python identifiers (`snake_case` methods, `Optional[T]`
instead of `param.Opt[T]`, etc.). Python-only changes (SDK internals,
Python-specific fixes) have no Go counterpart — write those fresh.
- **Do not copy** the auto-generated changelog verbatim. Aggregate related
changes. Skip noise.
- **Omit `codegen metadata` and `aggregated API specs update`** entries unless
Expand Down Expand Up @@ -218,9 +249,11 @@ After merge, `stainless-app[bot]` auto-creates a GitHub Release.
| Situation | Action |
|---|---|
| No open release PR | Inform user, stop |
| CI failing | Show failures, do not merge |
| `detect-breaking-changes` fails | Informational only. Report to user, proceed with merge |
| CI failing (other checks) | Show failures, do not merge |
| CI pending | Warn, ask user preference |
| Merge conflict | Report, suggest manual resolution |
| Merge fails | Report error, stop |
| Release not found after merge | Retry once after 10s, then report |
| Go SDK release not found | Proceed without cross-SDK reference |
| `gh` CLI not authenticated | Report, suggest `gh auth login` |
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.45.0"
".": "0.46.0"
}
8 changes: 4 additions & 4 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 658
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/gcore/gcore-63b63d8be16530f04e4c07443b8f3ed2554e65dd6343d1ac13a48f89c2cf9fc4.yml
openapi_spec_hash: 6f72f97fd9545f3ecda586de840c68ee
config_hash: 88e4af508ede520a45a0563d9cf077cc
configured_endpoints: 655
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/gcore/gcore-ca11e3a3caab61a2583bde810437de01f0635c3a48b7eceb20f910701452bc56.yml
openapi_spec_hash: eba662f80750410bacdbe0bcf4b8d875
config_hash: 24d55cb26d543a2d65d129ebe46f7775
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
# Changelog

## 0.46.0 (2026-05-08)

Full Changelog: [v0.45.0...v0.46.0](https://github.com/G-Core/gcore-python/compare/v0.45.0...v0.46.0)

### Features

* add cross-SDK sync and relax CI breaking-change check for /release skill ([227d229](https://github.com/G-Core/gcore-python/commit/227d229251317cfa2e9cc57d55e28f6bf85c6a7a))
* **api:** aggregated API specs update ([9d56917](https://github.com/G-Core/gcore-python/commit/9d56917be59224908e9953809511d9132a4a899b))
* **api:** aggregated API specs update ([6722396](https://github.com/G-Core/gcore-python/commit/6722396ffbc3a6d73433b6393e7f5e812d0de4f2))
* **api:** aggregated API specs update ([f5efa16](https://github.com/G-Core/gcore-python/commit/f5efa161e613f3dd5b9078edf87b1ff09c6d990b))
* **api:** aggregated API specs update ([adc6730](https://github.com/G-Core/gcore-python/commit/adc67307096350666c53204150fbb30a14cb9a7e))
* **cdn:** add client_config SDK subresource for /cdn/clients/me ([dd6eaa9](https://github.com/G-Core/gcore-python/commit/dd6eaa95eb50cfb5e8fb14938062d406851e16c5))


### Bug Fixes

* **client:** add missing f-string prefix in file type error message ([dcc262a](https://github.com/G-Core/gcore-python/commit/dcc262a3bd776078d57aa27b484b7f3982b68861))


### Chores

* **client:** rename cloud_polling_* opts to polling_* ([2fbb1d4](https://github.com/G-Core/gcore-python/commit/2fbb1d4841153fb0906e30ca3ccd4047a51f9e55))

## 0.45.0 (2026-05-04)

Full Changelog: [v0.44.0...v0.45.0](https://github.com/G-Core/gcore-python/compare/v0.44.0...v0.45.0)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "gcore"
version = "0.45.0"
version = "0.46.0"
description = "The official Python library for the gcore API"
dynamic = ["readme"]
license = "Apache-2.0"
Expand Down
56 changes: 28 additions & 28 deletions src/gcore/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,17 @@ class Gcore(SyncAPIClient):
api_key: str
cloud_project_id: int | None
cloud_region_id: int | None
cloud_polling_interval_seconds: int | None
cloud_polling_timeout_seconds: int | None
polling_interval_seconds: int | None
polling_timeout_seconds: int | None

def __init__(
self,
*,
api_key: str | None = None,
cloud_project_id: int | None = None,
cloud_region_id: int | None = None,
cloud_polling_interval_seconds: int | None = 3,
cloud_polling_timeout_seconds: int | None = 7200,
polling_interval_seconds: int | None = 3,
polling_timeout_seconds: int | None = 7200,
base_url: str | httpx.URL | None = None,
timeout: float | Timeout | None | NotGiven = not_given,
max_retries: int = DEFAULT_MAX_RETRIES,
Expand Down Expand Up @@ -108,13 +108,13 @@ def __init__(
cloud_region_id = maybe_coerce_integer(os.environ.get("GCORE_CLOUD_REGION_ID"))
self.cloud_region_id = cloud_region_id

if cloud_polling_interval_seconds is None:
cloud_polling_interval_seconds = 3
self.cloud_polling_interval_seconds = cloud_polling_interval_seconds
if polling_interval_seconds is None:
polling_interval_seconds = 3
self.polling_interval_seconds = polling_interval_seconds

if cloud_polling_timeout_seconds is None:
cloud_polling_timeout_seconds = 7200
self.cloud_polling_timeout_seconds = cloud_polling_timeout_seconds
if polling_timeout_seconds is None:
polling_timeout_seconds = 7200
self.polling_timeout_seconds = polling_timeout_seconds

if base_url is None:
base_url = os.environ.get("GCORE_BASE_URL")
Expand Down Expand Up @@ -233,8 +233,8 @@ def copy(
api_key: str | None = None,
cloud_project_id: int | None = None,
cloud_region_id: int | None = None,
cloud_polling_interval_seconds: int | None = None,
cloud_polling_timeout_seconds: int | None = None,
polling_interval_seconds: int | None = None,
polling_timeout_seconds: int | None = None,
base_url: str | httpx.URL | None = None,
timeout: float | Timeout | None | NotGiven = not_given,
http_client: httpx.Client | None = None,
Expand Down Expand Up @@ -271,8 +271,8 @@ def copy(
api_key=api_key or self.api_key,
cloud_project_id=cloud_project_id or self.cloud_project_id,
cloud_region_id=cloud_region_id or self.cloud_region_id,
cloud_polling_interval_seconds=cloud_polling_interval_seconds or self.cloud_polling_interval_seconds,
cloud_polling_timeout_seconds=cloud_polling_timeout_seconds or self.cloud_polling_timeout_seconds,
polling_interval_seconds=polling_interval_seconds or self.polling_interval_seconds,
polling_timeout_seconds=polling_timeout_seconds or self.polling_timeout_seconds,
base_url=base_url or self.base_url,
timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,
http_client=http_client,
Expand Down Expand Up @@ -343,17 +343,17 @@ class AsyncGcore(AsyncAPIClient):
api_key: str
cloud_project_id: int | None
cloud_region_id: int | None
cloud_polling_interval_seconds: int | None
cloud_polling_timeout_seconds: int | None
polling_interval_seconds: int | None
polling_timeout_seconds: int | None

def __init__(
self,
*,
api_key: str | None = None,
cloud_project_id: int | None = None,
cloud_region_id: int | None = None,
cloud_polling_interval_seconds: int | None = 3,
cloud_polling_timeout_seconds: int | None = 7200,
polling_interval_seconds: int | None = 3,
polling_timeout_seconds: int | None = 7200,
base_url: str | httpx.URL | None = None,
timeout: float | Timeout | None | NotGiven = not_given,
max_retries: int = DEFAULT_MAX_RETRIES,
Expand Down Expand Up @@ -396,13 +396,13 @@ def __init__(
cloud_region_id = maybe_coerce_integer(os.environ.get("GCORE_CLOUD_REGION_ID"))
self.cloud_region_id = cloud_region_id

if cloud_polling_interval_seconds is None:
cloud_polling_interval_seconds = 3
self.cloud_polling_interval_seconds = cloud_polling_interval_seconds
if polling_interval_seconds is None:
polling_interval_seconds = 3
self.polling_interval_seconds = polling_interval_seconds

if cloud_polling_timeout_seconds is None:
cloud_polling_timeout_seconds = 7200
self.cloud_polling_timeout_seconds = cloud_polling_timeout_seconds
if polling_timeout_seconds is None:
polling_timeout_seconds = 7200
self.polling_timeout_seconds = polling_timeout_seconds

if base_url is None:
base_url = os.environ.get("GCORE_BASE_URL")
Expand Down Expand Up @@ -521,8 +521,8 @@ def copy(
api_key: str | None = None,
cloud_project_id: int | None = None,
cloud_region_id: int | None = None,
cloud_polling_interval_seconds: int | None = None,
cloud_polling_timeout_seconds: int | None = None,
polling_interval_seconds: int | None = None,
polling_timeout_seconds: int | None = None,
base_url: str | httpx.URL | None = None,
timeout: float | Timeout | None | NotGiven = not_given,
http_client: httpx.AsyncClient | None = None,
Expand Down Expand Up @@ -559,8 +559,8 @@ def copy(
api_key=api_key or self.api_key,
cloud_project_id=cloud_project_id or self.cloud_project_id,
cloud_region_id=cloud_region_id or self.cloud_region_id,
cloud_polling_interval_seconds=cloud_polling_interval_seconds or self.cloud_polling_interval_seconds,
cloud_polling_timeout_seconds=cloud_polling_timeout_seconds or self.cloud_polling_timeout_seconds,
polling_interval_seconds=polling_interval_seconds or self.polling_interval_seconds,
polling_timeout_seconds=polling_timeout_seconds or self.polling_timeout_seconds,
base_url=base_url or self.base_url,
timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,
http_client=http_client,
Expand Down
2 changes: 1 addition & 1 deletion src/gcore/_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles
elif is_sequence_t(files):
files = [(key, await _async_transform_file(file)) for key, file in files]
else:
raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence")
raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence")

return files

Expand Down
2 changes: 1 addition & 1 deletion src/gcore/_version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

__title__ = "gcore"
__version__ = "0.45.0" # x-release-please-version
__version__ = "0.46.0" # x-release-please-version
14 changes: 14 additions & 0 deletions src/gcore/resources/cdn/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@
CDNResourcesResourceWithStreamingResponse,
AsyncCDNResourcesResourceWithStreamingResponse,
)
from .client_config import (
ClientConfigResource,
AsyncClientConfigResource,
ClientConfigResourceWithRawResponse,
AsyncClientConfigResourceWithRawResponse,
ClientConfigResourceWithStreamingResponse,
AsyncClientConfigResourceWithStreamingResponse,
)
from .logs_uploader import (
LogsUploaderResource,
AsyncLogsUploaderResource,
Expand Down Expand Up @@ -206,6 +214,12 @@
"AsyncIPsResourceWithRawResponse",
"IPsResourceWithStreamingResponse",
"AsyncIPsResourceWithStreamingResponse",
"ClientConfigResource",
"AsyncClientConfigResource",
"ClientConfigResourceWithRawResponse",
"AsyncClientConfigResourceWithRawResponse",
"ClientConfigResourceWithStreamingResponse",
"AsyncClientConfigResourceWithStreamingResponse",
"CDNResource",
"AsyncCDNResource",
"CDNResourceWithRawResponse",
Expand Down
6 changes: 6 additions & 0 deletions src/gcore/resources/cdn/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,3 +317,9 @@ from gcore.types.cdn import PublicIPList
Methods:

- <code title="get /cdn/public-ip-list">client.cdn.ips.<a href="./src/gcore/resources/cdn/ips.py">list</a>(\*\*<a href="src/gcore/types/cdn/ip_list_params.py">params</a>) -> <a href="./src/gcore/types/cdn/public_ip_list.py">PublicIPList</a></code>

## ClientConfig

Methods:

- <code title="get /cdn/clients/me">client.cdn.client_config.<a href="./src/gcore/resources/cdn/client_config.py">get</a>() -> <a href="./src/gcore/types/cdn/cdn_account.py">CDNAccount</a></code>
Loading
Loading