From 622c65e6334f628d60e5c3aa433bc37b38eb3619 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 14:32:36 +0000 Subject: [PATCH 1/7] feat: Merge pull request #149 from trycourier/geraldosilva/c-19201-notification-template-subscription-topic-id Document subscription_topic_id on notification template V2 responses --- .stats.yml | 4 ++-- .../NotificationTemplateSummary.php | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 9e3b99b6..32f52ad6 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 138 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier/courier-11e1931d767cef402bbed5a965746d1933ae72db0f41881860593a0b1424fd80.yml -openapi_spec_hash: 9def941805f233f8761c4bdae88f6137 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier/courier-1ba05406f99c844cf7b32fe80c399017d00c504563f211caf66dfbccf8dd146d.yml +openapi_spec_hash: be1b9b8a351560da21bbe3b53fd04905 config_hash: 6c3a754258f0f77e9032a90a478ab76d diff --git a/src/Notifications/NotificationTemplateSummary.php b/src/Notifications/NotificationTemplateSummary.php index 3a97bb81..cb32db60 100644 --- a/src/Notifications/NotificationTemplateSummary.php +++ b/src/Notifications/NotificationTemplateSummary.php @@ -20,6 +20,7 @@ * name: string, * state: State|value-of, * tags: list, + * subscriptionTopicID?: string|null, * updated?: int|null, * updater?: string|null, * } @@ -55,6 +56,12 @@ final class NotificationTemplateSummary implements BaseModel #[Required(list: 'string')] public array $tags; + /** + * The linked subscription (preference) topic of the published version. Omitted when no topic is linked or the template has never been published. + */ + #[Optional('subscription_topic_id')] + public ?string $subscriptionTopicID; + /** * Epoch milliseconds of last update. */ @@ -109,6 +116,7 @@ public static function with( string $name, State|string $state, array $tags, + ?string $subscriptionTopicID = null, ?int $updated = null, ?string $updater = null, ): self { @@ -121,6 +129,7 @@ public static function with( $self['state'] = $state; $self['tags'] = $tags; + null !== $subscriptionTopicID && $self['subscriptionTopicID'] = $subscriptionTopicID; null !== $updated && $self['updated'] = $updated; null !== $updater && $self['updater'] = $updater; @@ -187,6 +196,17 @@ public function withTags(array $tags): self return $self; } + /** + * The linked subscription (preference) topic of the published version. Omitted when no topic is linked or the template has never been published. + */ + public function withSubscriptionTopicID(string $subscriptionTopicID): self + { + $self = clone $this; + $self['subscriptionTopicID'] = $subscriptionTopicID; + + return $self; + } + /** * Epoch milliseconds of last update. */ From be965adeb3c022bd988e03a976d2842634726a5c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 18:39:38 +0000 Subject: [PATCH 2/7] feat: Document POST /notifications/{id}/duplicate [C-18607] (#165) * Document POST /notifications/{id}/duplicate [C-18607] Documents the V2 notification template duplicate endpoint added in trycourier/backend#9536 and registers it for SDK generation. - openapi.yml: new /notifications/{id}/duplicate path (post, operationId notifications_duplicate), modeled on the sibling publish endpoint. 201 -> NotificationTemplateResponse, 404 -> NotFound, 422 -> UnprocessableEntity (source is journey- or broadcast-scoped). - stainless.yml: adds duplicate: post /notifications/{id}/duplicate so a duplicate() method generates across the SDKs. Co-Authored-By: Claude Opus 4.8 (1M context) * Use real TypeID-style example IDs + add id-conventions.md The duplicate endpoint example used placeholder IDs (nt_01xyz789, brand_abc, marketing, nt_123, journey_abc) that don't match the TypeID convention the rest of the spec follows. Swap them for real canonical sample IDs (nt_/bnd_/pt_/jry_). Add specs/id-conventions.md documenting the prefix registry (sourced from backend lib/courier-ids/constants.ts), the canonical sample IDs to reuse, and two open questions (journey jry vs jny; brand bnd not yet in the backend registry). Co-Authored-By: Claude Opus 4.8 (1M context) * Move ID conventions from specs/ into an example-ids skill specs/ is for the shipped spec, not editor/agent guidance. Relocate the convention reference to .agents/skills/example-ids/SKILL.md (surfaced via the .claude/.cursor/.codex skills symlinks) so it triggers when editing openapi.yml examples. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .stats.yml | 8 +++--- .../NotificationsContract.php | 13 ++++++++++ .../NotificationsRawContract.php | 15 +++++++++++ src/Services/NotificationsRawService.php | 25 +++++++++++++++++++ src/Services/NotificationsService.php | 20 +++++++++++++++ tests/Services/NotificationsTest.php | 13 ++++++++++ 6 files changed, 90 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index 32f52ad6..809d9f7f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 138 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier/courier-1ba05406f99c844cf7b32fe80c399017d00c504563f211caf66dfbccf8dd146d.yml -openapi_spec_hash: be1b9b8a351560da21bbe3b53fd04905 -config_hash: 6c3a754258f0f77e9032a90a478ab76d +configured_endpoints: 139 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier/courier-def1e982add0411bf64b48cdf5e4f2c1b85b9bd0ada0d5e6fc4869afa85e0bff.yml +openapi_spec_hash: 5d129cdf800798bcd4a93585ec89c696 +config_hash: dcd58abf1e56b114bdce22336ca3d570 diff --git a/src/ServiceContracts/NotificationsContract.php b/src/ServiceContracts/NotificationsContract.php index 50c98c5e..eed714d6 100644 --- a/src/ServiceContracts/NotificationsContract.php +++ b/src/ServiceContracts/NotificationsContract.php @@ -86,6 +86,19 @@ public function archive( RequestOptions|array|null $requestOptions = null ): mixed; + /** + * @api + * + * @param string $id template ID (nt_ prefix) + * @param RequestOpts|null $requestOptions + * + * @throws APIException + */ + public function duplicate( + string $id, + RequestOptions|array|null $requestOptions = null + ): NotificationTemplateResponse; + /** * @api * diff --git a/src/ServiceContracts/NotificationsRawContract.php b/src/ServiceContracts/NotificationsRawContract.php index cfbb01c3..af9cee75 100644 --- a/src/ServiceContracts/NotificationsRawContract.php +++ b/src/ServiceContracts/NotificationsRawContract.php @@ -91,6 +91,21 @@ public function archive( RequestOptions|array|null $requestOptions = null ): BaseResponse; + /** + * @api + * + * @param string $id template ID (nt_ prefix) + * @param RequestOpts|null $requestOptions + * + * @return BaseResponse + * + * @throws APIException + */ + public function duplicate( + string $id, + RequestOptions|array|null $requestOptions = null + ): BaseResponse; + /** * @api * diff --git a/src/Services/NotificationsRawService.php b/src/Services/NotificationsRawService.php index 8fd428f6..1eed20cb 100644 --- a/src/Services/NotificationsRawService.php +++ b/src/Services/NotificationsRawService.php @@ -172,6 +172,31 @@ public function archive( ); } + /** + * @api + * + * Duplicate a notification template. Creates a standalone copy within the same workspace and environment, with " COPY" appended to the title. The copy clones the source draft's tags, brand, subscription topic, routing strategy, channels, and content, and is always created as a standalone template (it is not linked to any journey or broadcast, even if the source was). Templates that are scoped to a journey or a broadcast cannot be duplicated through this endpoint. + * + * @param string $id template ID (nt_ prefix) + * @param RequestOpts|null $requestOptions + * + * @return BaseResponse + * + * @throws APIException + */ + public function duplicate( + string $id, + RequestOptions|array|null $requestOptions = null + ): BaseResponse { + // @phpstan-ignore-next-line return.type + return $this->client->request( + method: 'post', + path: ['notifications/%1$s/duplicate', $id], + options: $requestOptions, + convert: NotificationTemplateResponse::class, + ); + } + /** * @api * diff --git a/src/Services/NotificationsService.php b/src/Services/NotificationsService.php index 534e97a9..36d1d733 100644 --- a/src/Services/NotificationsService.php +++ b/src/Services/NotificationsService.php @@ -147,6 +147,26 @@ public function archive( return $response->parse(); } + /** + * @api + * + * Duplicate a notification template. Creates a standalone copy within the same workspace and environment, with " COPY" appended to the title. The copy clones the source draft's tags, brand, subscription topic, routing strategy, channels, and content, and is always created as a standalone template (it is not linked to any journey or broadcast, even if the source was). Templates that are scoped to a journey or a broadcast cannot be duplicated through this endpoint. + * + * @param string $id template ID (nt_ prefix) + * @param RequestOpts|null $requestOptions + * + * @throws APIException + */ + public function duplicate( + string $id, + RequestOptions|array|null $requestOptions = null + ): NotificationTemplateResponse { + // @phpstan-ignore-next-line argument.type + $response = $this->raw->duplicate($id, requestOptions: $requestOptions); + + return $response->parse(); + } + /** * @api * diff --git a/tests/Services/NotificationsTest.php b/tests/Services/NotificationsTest.php index 5e9ce3c9..01964184 100644 --- a/tests/Services/NotificationsTest.php +++ b/tests/Services/NotificationsTest.php @@ -118,6 +118,19 @@ public function testArchive(): void $this->assertNull($result); } + #[Test] + public function testDuplicate(): void + { + if (UnsupportedMockTests::$skip) { + $this->markTestSkipped('Mock server tests are disabled'); + } + + $result = $this->client->notifications->duplicate('id'); + + // @phpstan-ignore-next-line method.alreadyNarrowedType + $this->assertInstanceOf(NotificationTemplateResponse::class, $result); + } + #[Test] public function testListVersions(): void { From 69bfa6b0e807f5120d48dcc1cc7aa1f532ec0a9c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 18 Jul 2026 02:02:09 +0000 Subject: [PATCH 3/7] feat(stlc): configurable CI runner and private-production-repo support in workflow templates --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 210fc7f4..84d8fb1c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: lint: timeout-minutes: 10 name: lint - runs-on: ${{ github.repository == 'stainless-sdks/courier-php' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: @@ -37,7 +37,7 @@ jobs: test: timeout-minutes: 10 name: test - runs-on: ${{ github.repository == 'stainless-sdks/courier-php' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 From d13b756676da6a5cc7eb7ce07d6fe6c4fdea8183 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 17:21:56 +0000 Subject: [PATCH 4/7] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 809d9f7f..11f22e86 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 139 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier/courier-def1e982add0411bf64b48cdf5e4f2c1b85b9bd0ada0d5e6fc4869afa85e0bff.yml -openapi_spec_hash: 5d129cdf800798bcd4a93585ec89c696 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier/courier-15e6e3c944208ca5d7bad9070f5c1e04774bf21e64454244f98e7b9e56baf70b.yml +openapi_spec_hash: 2cc798d9c07f90dd28ab9c92854c7947 config_hash: dcd58abf1e56b114bdce22336ca3d570 From 7dd1c9069dc3c8491ec7dbabd286148eef1951c0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 18:44:20 +0000 Subject: [PATCH 5/7] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 11f22e86..705268ee 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 139 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier/courier-15e6e3c944208ca5d7bad9070f5c1e04774bf21e64454244f98e7b9e56baf70b.yml -openapi_spec_hash: 2cc798d9c07f90dd28ab9c92854c7947 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier/courier-3ba276bda58608efe8027da0df37d24407f9995a759aa10e4a6cb201a40e8e4d.yml +openapi_spec_hash: 5e207aa1972d01137897cc20240d2c9d config_hash: dcd58abf1e56b114bdce22336ca3d570 From 9f7f082b3f2f87033f9d1242aed96ebcdf141485 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 18:45:21 +0000 Subject: [PATCH 6/7] feat: Merge pull request #168 from trycourier/geraldosilva/c-19419-topic-id-notification-template-summary Document topic_id on NotificationTemplateSummary (C-19419) --- .stats.yml | 4 ++-- .../NotificationTemplateSummary.php | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 705268ee..44174988 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 139 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier/courier-3ba276bda58608efe8027da0df37d24407f9995a759aa10e4a6cb201a40e8e4d.yml -openapi_spec_hash: 5e207aa1972d01137897cc20240d2c9d +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier/courier-b03f5346718dcd7b22f15893445ea593ab87535c90c540fc5a796c6ecd54d779.yml +openapi_spec_hash: 15677d3be4ecd33eab154080b4bc437f config_hash: dcd58abf1e56b114bdce22336ca3d570 diff --git a/src/Notifications/NotificationTemplateSummary.php b/src/Notifications/NotificationTemplateSummary.php index cb32db60..d94a3aee 100644 --- a/src/Notifications/NotificationTemplateSummary.php +++ b/src/Notifications/NotificationTemplateSummary.php @@ -21,6 +21,7 @@ * state: State|value-of, * tags: list, * subscriptionTopicID?: string|null, + * topicID?: string|null, * updated?: int|null, * updater?: string|null, * } @@ -62,6 +63,12 @@ final class NotificationTemplateSummary implements BaseModel #[Optional('subscription_topic_id')] public ?string $subscriptionTopicID; + /** + * Alias of subscription_topic_id, provided under the same name V1 list items use for the linked topic. Always carries the same value as subscription_topic_id. + */ + #[Optional('topic_id')] + public ?string $topicID; + /** * Epoch milliseconds of last update. */ @@ -117,6 +124,7 @@ public static function with( State|string $state, array $tags, ?string $subscriptionTopicID = null, + ?string $topicID = null, ?int $updated = null, ?string $updater = null, ): self { @@ -130,6 +138,7 @@ public static function with( $self['tags'] = $tags; null !== $subscriptionTopicID && $self['subscriptionTopicID'] = $subscriptionTopicID; + null !== $topicID && $self['topicID'] = $topicID; null !== $updated && $self['updated'] = $updated; null !== $updater && $self['updater'] = $updater; @@ -207,6 +216,17 @@ public function withSubscriptionTopicID(string $subscriptionTopicID): self return $self; } + /** + * Alias of subscription_topic_id, provided under the same name V1 list items use for the linked topic. Always carries the same value as subscription_topic_id. + */ + public function withTopicID(string $topicID): self + { + $self = clone $this; + $self['topicID'] = $topicID; + + return $self; + } + /** * Epoch milliseconds of last update. */ From a2f854265cdedbd56a5a7585595d28b4682da09a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 18:45:46 +0000 Subject: [PATCH 7/7] release: 5.12.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 16 ++++++++++++++++ src/Version.php | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 1363fe01..8f918e4b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "5.11.2" + ".": "5.12.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c9bf1084..5b31308a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 5.12.0 (2026-07-20) + +Full Changelog: [v5.11.2...v5.12.0](https://github.com/trycourier/courier-php/compare/v5.11.2...v5.12.0) + +### Features + +* Document POST /notifications/{id}/duplicate [C-18607] ([#165](https://github.com/trycourier/courier-php/issues/165)) ([be965ad](https://github.com/trycourier/courier-php/commit/be965adeb3c022bd988e03a976d2842634726a5c)) +* Merge pull request [#149](https://github.com/trycourier/courier-php/issues/149) from trycourier/geraldosilva/c-19201-notification-template-subscription-topic-id ([622c65e](https://github.com/trycourier/courier-php/commit/622c65e6334f628d60e5c3aa433bc37b38eb3619)) +* Merge pull request [#168](https://github.com/trycourier/courier-php/issues/168) from trycourier/geraldosilva/c-19419-topic-id-notification-template-summary ([9f7f082](https://github.com/trycourier/courier-php/commit/9f7f082b3f2f87033f9d1242aed96ebcdf141485)) +* **stlc:** configurable CI runner and private-production-repo support in workflow templates ([69bfa6b](https://github.com/trycourier/courier-php/commit/69bfa6b0e807f5120d48dcc1cc7aa1f532ec0a9c)) + + +### Bug Fixes + +* **ci:** drop Packagist secret requirement from release doctor ([#90](https://github.com/trycourier/courier-php/issues/90)) ([e0b992d](https://github.com/trycourier/courier-php/commit/e0b992df2ac46603779dc3c566dd178c558f42c4)) + ## 5.11.2 (2026-07-10) Full Changelog: [v5.11.1...v5.11.2](https://github.com/trycourier/courier-php/compare/v5.11.1...v5.11.2) diff --git a/src/Version.php b/src/Version.php index 256e7687..2f86aeaa 100644 --- a/src/Version.php +++ b/src/Version.php @@ -5,5 +5,5 @@ namespace Courier; // x-release-please-start-version -const VERSION = '5.11.2'; +const VERSION = '5.12.0'; // x-release-please-end